z_mirror/PipeVisualizer

git clone https://git.raiguard.me/z_mirror/PipeVisualizer

Caleb HeuerUpdate require syntax9443616

main
9.4 KiB368 linesraw
1local flib_math = require("__flib__.math")
2local flib_queue = require("__flib__.queue")
3
4local entity_data = require("scripts.entity-data")
5local renderer = require("scripts.renderer")
6
7--- @alias FlowDirection "input"|"output"|"input-output"
8--- @alias FluidSystemID uint
9--- @alias PlayerIndex uint
10
11--- @class Iterator
12--- @field entities table<UnitNumber, EntityData>
13--- @field in_overlay boolean
14--- @field in_queue table<UnitNumber, boolean>
15--- @field next_color_index integer
16--- @field player_index PlayerIndex
17--- @field queue Queue<ToIterateData>
18--- @field systems table<FluidSystemID, FluidSystemData?>
19--- @field to_ignore table<UnitNumber, boolean>
20
21--- @class FluidSystemData
22--- @field color Color
23--- @field from_hover boolean
24--- @field order uint
25
26--- @class ToIterateData
27--- @field entity LuaEntity
28--- @field unit_number UnitNumber
29
30--- @param iterator Iterator
31--- @param entity LuaEntity
32local function push(iterator, entity)
33  if not entity.valid then
34    return
35  end
36  local unit_number = entity.unit_number --[[@as UnitNumber]]
37  iterator.in_queue[unit_number] = true
38  flib_queue.push_back(iterator.queue, { entity = entity, unit_number = unit_number })
39end
40
41--- @param iterator Iterator
42--- @return LuaEntity?
43local function pop(iterator)
44  while true do
45    local to_iterate = flib_queue.pop_front(iterator.queue)
46    if not to_iterate then
47      return
48    end
49    local unit_number = to_iterate.unit_number
50    iterator.in_queue[unit_number] = nil
51    if iterator.to_ignore[unit_number] then
52      iterator.to_ignore[unit_number] = nil
53    elseif to_iterate.entity.valid then
54      return to_iterate.entity
55    end
56  end
57end
58
59--- @param entity LuaEntity
60--- @param player_index PlayerIndex
61--- @param in_overlay boolean
62--- @param from_hover boolean?
63--- @return boolean accepted
64local function request(entity, player_index, in_overlay, from_hover)
65  if not global.iterator then
66    return false
67  end
68
69  if global.blacklist[entity.name] then
70    return false
71  end
72
73  local self = global.iterator[player_index]
74  if not self then
75    --- @type Iterator
76    self = {
77      entities = {},
78      in_overlay = in_overlay,
79      in_queue = {},
80      next_color_index = 0,
81      player_index = player_index,
82      queue = flib_queue.new(),
83      scheduled = {},
84      systems = {},
85      to_ignore = {},
86    }
87  end
88
89  local unit_number = entity.unit_number
90  if not unit_number then
91    return false
92  end
93  if self.to_ignore[unit_number] then
94    self.to_ignore[unit_number] = nil
95  end
96  if self.entities[unit_number] or self.in_queue[unit_number] then
97    return self.in_overlay -- To handle entities that cross chunk boundaries
98  end
99
100  from_hover = from_hover or false
101
102  local fluidbox = entity.fluidbox
103  local should_iterate = false
104  for fluidbox_index = 1, #fluidbox do
105    local fluid_system_id = fluidbox.get_fluid_system_id(fluidbox_index)
106    if not fluid_system_id then
107      goto continue
108    end
109    local system = self.systems[fluid_system_id]
110    if system and not in_overlay then
111      goto continue
112    end
113
114    if not system then
115      local color = { r = 0.4, g = 0.4, b = 0.4 }
116      local order = flib_math.max_uint
117      if global.color_by_system[self.player_index] then
118        self.next_color_index = self.next_color_index + 1
119        local next_color = global.system_colors[self.next_color_index]
120        if next_color then
121          color = next_color
122          order = self.next_color_index
123        end
124      else
125        local contents = fluidbox.get_fluid_system_contents(fluidbox_index)
126        if contents and next(contents) then
127          color = global.fluid_colors[next(contents)]
128          order = global.fluid_order[next(contents)]
129        end
130      end
131
132      self.systems[fluid_system_id] = { color = color, from_hover = from_hover, order = order }
133    end
134
135    should_iterate = true
136
137    ::continue::
138  end
139
140  if should_iterate then
141    push(self, entity)
142    global.iterator[player_index] = self
143  end
144
145  return should_iterate
146end
147
148--- @param iterator Iterator
149--- @param entities_per_tick integer
150local function iterate(iterator, entities_per_tick)
151  for _ = 1, entities_per_tick do
152    local entity = pop(iterator)
153    if not entity then
154      break
155    end
156
157    -- If the entity data already existed, this entity was requested to be redrawn
158    local data = entity_data.create(iterator, entity)
159    if not data then
160      return
161    end
162
163    renderer.draw(iterator, data)
164
165    if iterator.in_overlay or iterator.in_queue[entity.unit_number] then
166      goto continue
167    end
168
169    -- Propagate to undrawn neighbours
170    for fluid_system_id, connections in pairs(data.connections) do
171      if iterator.systems[fluid_system_id] then
172        for _, connection in pairs(connections) do
173          local owner = connection.target_owner
174          if owner and not global.blacklist[owner.name] then
175            local data = entity_data.get(iterator, owner)
176            if not data or data.connections[fluid_system_id] and not data.connection_objects[fluid_system_id] then
177              local unit_number = owner.unit_number --[[@as UnitNumber]]
178              if not iterator.in_queue[unit_number] then
179                push(iterator, owner)
180              end
181            end
182          end
183        end
184      end
185    end
186
187    ::continue::
188  end
189end
190
191--- @param iterator Iterator
192--- @param fluid_system_id FluidSystemID
193local function clear_system(iterator, fluid_system_id)
194  iterator.systems[fluid_system_id] = nil
195  for _, data in pairs(iterator.entities) do
196    if data.connections[fluid_system_id] then
197      entity_data.remove_system(iterator, data, fluid_system_id)
198    end
199  end
200  if not next(iterator.systems) then
201    global.iterator[iterator.player_index] = nil
202  end
203end
204
205--- @param player_index PlayerIndex
206--- @param entity LuaEntity
207local function clear(player_index, entity)
208  if not global.iterator or not entity.valid then
209    return
210  end
211  local self = global.iterator[player_index]
212  if not self then
213    return
214  end
215  local unit_number = entity.unit_number
216  if not unit_number then
217    return
218  end
219  if self.in_queue[unit_number] then
220    self.to_ignore[unit_number] = true
221  end
222  local data = entity_data.get(self, entity)
223  if not data then
224    return
225  end
226  entity_data.remove(self, data)
227end
228
229--- @param player_index PlayerIndex
230local function clear_all(player_index)
231  if not global.iterator then
232    return
233  end
234  local it = global.iterator[player_index]
235  if not it then
236    return
237  end
238  for _, entity_data in pairs(it.entities) do
239    renderer.clear(entity_data)
240  end
241  global.iterator[player_index] = nil
242  if not next(global.iterator) then
243    renderer.reset()
244  end
245end
246
247--- @param player_index PlayerIndex
248local function clear_hovered(player_index)
249  if not global.iterator then
250    return
251  end
252  local it = global.iterator[player_index]
253  if not it then
254    return
255  end
256  for system_id, system_data in pairs(it.systems) do
257    if system_data.from_hover then
258      clear_system(it, system_id)
259    end
260  end
261  if not next(it.systems) then
262    global.iterator[player_index] = nil
263    if not next(global.iterator) then
264      renderer.reset()
265    end
266  end
267end
268
269--- @param player_index PlayerIndex
270--- @return Iterator?
271local function get(player_index)
272  if not global.iterator then
273    return
274  end
275  return global.iterator[player_index]
276end
277
278--- @param entity LuaEntity
279--- @param player_index PlayerIndex
280local function request_or_clear(entity, player_index)
281  local it = get(player_index)
282  if not it then
283    request(entity, player_index, false)
284    return
285  end
286  if it.in_overlay then
287    return
288  end
289  -- If this entity was being hovered, change its systems to persistent instead of removing the visualization.
290  local entity_data = entity_data.get(it, entity)
291  if entity_data then
292    local did_change
293    for fluid_system_id in pairs(entity_data.connections) do
294      local system_data = it.systems[fluid_system_id]
295      if system_data and system_data.from_hover then
296        system_data.from_hover = false
297        did_change = true
298      end
299    end
300    if did_change then
301      return
302    end
303  end
304  if request(entity, player_index, false) then
305    return
306  end
307  local fluidbox = entity.fluidbox
308  for fluidbox_index = 1, #fluidbox do
309    --- @cast fluidbox_index uint
310    local id = fluidbox.get_fluid_system_id(fluidbox_index)
311    if id and it.systems[id] then
312      clear_system(it, id)
313    end
314  end
315end
316
317local function on_tick()
318  if not global.iterator then
319    return
320  end
321  local entities_per_tick =
322    math.ceil(settings.global["pv-entities-per-tick"].value --[[@as int]] / table_size(global.iterator))
323  for _, iterator in pairs(global.iterator) do
324    iterate(iterator, entities_per_tick)
325  end
326end
327
328--- @param e EventData.CustomInputEvent
329local function on_toggle_hover(e)
330  local iterator = global.iterator[e.player_index]
331  if iterator and iterator.in_overlay then
332    return
333  end
334  local player = game.get_player(e.player_index)
335  if not player then
336    return
337  end
338  local entity = player.selected
339  if not entity then
340    clear_all(e.player_index)
341    return
342  end
343  request_or_clear(entity, e.player_index)
344end
345
346local function reset()
347  --- @type table<PlayerIndex, Iterator>
348  global.iterator = {}
349end
350
351--- @class iterator
352local iterator = {}
353
354iterator.on_init = reset
355iterator.on_configuration_changed = reset
356
357iterator.events = {
358  [defines.events.on_tick] = on_tick,
359  ["pv-visualize-selected"] = on_toggle_hover,
360}
361
362iterator.clear = clear
363iterator.clear_all = clear_all
364iterator.clear_hovered = clear_hovered
365iterator.get = get
366iterator.request = request
367
368return iterator