z_mirror/PipeVisualizer

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

Caleb HeuerRe-create overlay if it was destroyed73a26f6

main
8.5 KiB290 linesraw
1local flib_bounding_box = require("__flib__.bounding-box")
2local flib_math = require("__flib__.math")
3local flib_position = require("__flib__.position")
4local flib_table = require("__flib__.table")
5
6local iterator = require("scripts.iterator")
7
8-- In the source code, 200 is defined as the maximum viewable distance, but in reality it's around 220
9-- Map editor is 3x that, but we will ignore that for now
10-- Add five for a comfortable margin
11local max_overlay_size = 220 + 5
12
13--- @alias UnitNumber uint
14--- @alias EntityChunkPositionLookup table<int, LuaEntity[]?>
15
16--- @class Overlay
17--- @field background RenderObjectID
18--- @field entities_x table<int, LuaEntity[]>
19--- @field entities_y table<int, LuaEntity[]>
20--- @field dimensions DisplayResolution
21--- @field last_position MapPosition?
22--- @field player LuaPlayer
23
24--- @param self Overlay
25--- @param position MapPosition
26local function get_areas(self, position)
27  local dimensions = self.dimensions
28  local last_position = self.last_position
29  if not last_position then
30    return { flib_bounding_box.from_dimensions(position, dimensions.width, dimensions.height) }
31  end
32  local position_x = position.x
33  local position_y = position.y
34  local radius_x = dimensions.width / 2
35  local radius_y = dimensions.height / 2
36
37  local delta = flib_position.sub(position, last_position)
38  --- @type BoundingBox[]
39  local areas = {}
40
41  if delta.x < 0 then
42    areas[#areas + 1] = {
43      left_top = { x = position_x - radius_x, y = position_y - radius_y },
44      right_bottom = { x = last_position.x - radius_x, y = position_y + radius_y },
45    }
46  elseif delta.x > 0 then
47    areas[#areas + 1] = {
48      left_top = { x = last_position.x + radius_x, y = position_y - radius_y },
49      right_bottom = { x = position_x + radius_x, y = position_y + radius_y },
50    }
51  end
52
53  if delta.y < 0 then
54    areas[#areas + 1] = {
55      left_top = { x = position_x - radius_x - math.min(delta.x, 0), y = position_y - radius_y },
56      right_bottom = { x = position_x + radius_x - math.max(delta.x, 0), y = last_position.y - radius_y },
57    }
58  elseif delta.y > 0 then
59    areas[#areas + 1] = {
60      left_top = { x = position_x - radius_x - math.min(delta.x, 0), y = last_position.y + radius_y },
61      right_bottom = { x = position_x + radius_x - math.max(delta.x, 0), y = position_y + radius_y },
62    }
63  end
64
65  return areas
66end
67
68--- @param player LuaPlayer
69--- @return DisplayResolution
70local function get_dimensions(player)
71  local resolution = player.display_resolution
72  local divisor = math.max(resolution.width, resolution.height) / max_overlay_size
73  resolution.width = flib_math.ceiled(resolution.width / divisor, 64) + 32
74  resolution.height = flib_math.ceiled(resolution.height / divisor, 64) + 32
75  return resolution
76end
77
78--- @param self Overlay
79--- @param area BoundingBox
80local function visualize_area(self, area)
81  local entities = self.player.surface.find_entities_filtered({
82    area = area,
83    force = self.player.force,
84    type = {
85      "assembling-machine",
86      "boiler",
87      "fluid-turret",
88      "furnace",
89      "generator",
90      "infinity-pipe",
91      "inserter",
92      "lab",
93      "loader",
94      "loader-1x1",
95      "mining-drill",
96      "offshore-pump",
97      "pipe",
98      "pipe-to-ground",
99      "pump",
100      "reactor",
101      "rocket-silo",
102      "storage-tank",
103    },
104  })
105  for _, entity in pairs(entities) do
106    if iterator.request(entity, self.player.index, true) then
107      local chunk_position = flib_position.to_chunk(entity.position)
108      local x_lookup = flib_table.get_or_insert(self.entities_x, chunk_position.x, {})
109      x_lookup[#x_lookup + 1] = entity
110      local y_lookup = flib_table.get_or_insert(self.entities_y, chunk_position.y, {})
111      y_lookup[#y_lookup + 1] = entity
112    end
113  end
114end
115
116--- @param self Overlay
117--- @param lookup EntityChunkPositionLookup
118--- @param min int
119--- @param max int
120local function remove_out_of_bounds(self, lookup, min, max)
121  for pos, entities in pairs(lookup) do
122    if pos < min or pos >= max then
123      for _, entity in pairs(entities) do
124        iterator.clear(self.player.index, entity)
125      end
126      lookup[pos] = nil
127    end
128  end
129end
130
131--- @param self Overlay
132local function update_overlay(self)
133  local pos = self.player.position
134  local position = {
135    x = flib_math.floored(pos.x, 32) + 16,
136    y = flib_math.floored(pos.y, 32) + 16,
137  }
138  if self.last_position and flib_position.eq(position, self.last_position) then
139    return
140  end
141  if not rendering.is_valid(self.background) then
142    self.background = rendering.draw_sprite({
143      sprite = "pv-overlay-box",
144      tint = {
145        a = self.player.mod_settings["pv-overlay-opacity"].value --[[@as double]],
146      },
147      render_layer = "191",
148      target = self.player.position,
149      surface = self.player.surface,
150      players = { self.player },
151    })
152  end
153  rendering.set_target(self.background, position)
154  rendering.set_x_scale(self.background, self.dimensions.width)
155  rendering.set_y_scale(self.background, self.dimensions.height)
156
157  local areas = get_areas(self, position)
158
159  self.last_position = position
160
161  for _, area in pairs(areas) do
162    visualize_area(self, area)
163  end
164
165  remove_out_of_bounds(
166    self,
167    self.entities_x,
168    math.floor((position.x - (self.dimensions.width / 2)) / 32),
169    math.ceil((position.x + (self.dimensions.width / 2)) / 32)
170  )
171  remove_out_of_bounds(
172    self,
173    self.entities_y,
174    math.floor((position.y - (self.dimensions.height / 2)) / 32),
175    math.ceil((position.y + (self.dimensions.height / 2)) / 32)
176  )
177end
178
179--- @param player LuaPlayer
180--- @return Overlay
181local function create_overlay(player)
182  iterator.clear_all(player.index)
183  local opacity = player.mod_settings["pv-overlay-opacity"].value --[[@as double]]
184  local background = rendering.draw_sprite({
185    sprite = "pv-overlay-box",
186    tint = { a = opacity },
187    render_layer = "191",
188    target = player.position,
189    surface = player.surface,
190    players = { player },
191  })
192  --- @type Overlay
193  local self = {
194    background = background,
195    entities_x = {},
196    entities_y = {},
197    dimensions = get_dimensions(player),
198    player = player,
199  }
200  global.overlay[player.index] = self
201  update_overlay(self)
202  return self
203end
204
205--- @param self Overlay
206local function destroy_overlay(self)
207  rendering.destroy(self.background)
208  iterator.clear_all(self.player.index)
209  global.overlay[self.player.index] = nil
210end
211
212--- @param e EventData.CustomInputEvent|EventData.on_lua_shortcut
213local function on_toggle_overlay(e)
214  if (e.input_name or e.prototype_name) ~= "pv-toggle-overlay" then
215    return
216  end
217  local player = game.get_player(e.player_index)
218  if not player then
219    return
220  end
221  local entity = player.selected
222  if entity and entity.prototype.belt_speed and script.active_mods["belt-visualizer"] then
223    return
224  end
225  local cursor_stack = player.cursor_stack
226  if cursor_stack and cursor_stack.valid_for_read and (cursor_stack.is_blueprint_book or cursor_stack.is_blueprint) then
227    return
228  end
229  local self = global.overlay[e.player_index]
230  if self then
231    destroy_overlay(self)
232  else
233    create_overlay(player)
234  end
235  player.set_shortcut_toggled("pv-toggle-overlay", global.overlay[e.player_index] ~= nil)
236end
237
238--- @param e EventData.on_player_changed_position
239local function on_player_changed_position(e)
240  local self = global.overlay[e.player_index]
241  if self then
242    update_overlay(self)
243  end
244end
245
246--- @param e EventData.on_player_display_resolution_changed
247local function on_player_display_resolution_changed(e)
248  if not global.overlay then
249    return
250  end
251  local self = global.overlay[e.player_index]
252  if not self then
253    return
254  end
255  destroy_overlay(self)
256  create_overlay(self.player)
257end
258
259--- @param e EventData.CustomInputEvent
260local function on_color_by_system_pressed(e)
261  local self = global.overlay[e.player_index]
262  if not self then
263    return
264  end
265  destroy_overlay(self)
266  create_overlay(game.get_player(e.player_index) --[[@as LuaPlayer]])
267end
268
269local function reset()
270  --- @type table<uint, Overlay>
271  global.overlay = {}
272  for _, player in pairs(game.players) do
273    player.set_shortcut_toggled("pv-toggle-overlay", false)
274  end
275end
276
277local overlay = {}
278
279overlay.on_init = reset
280overlay.on_configuration_changed = reset
281
282overlay.events = {
283  [defines.events.on_lua_shortcut] = on_toggle_overlay,
284  [defines.events.on_player_changed_position] = on_player_changed_position,
285  [defines.events.on_player_display_resolution_changed] = on_player_display_resolution_changed,
286  ["pv-toggle-overlay"] = on_toggle_overlay,
287  ["pv-color-by-fluid-system"] = on_color_by_system_pressed,
288}
289
290return overlay