z_mirror/Tapeline

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

Caleb Heuerfix: Undefined function in 3.1.1 migrationd9815be

trunk
19.0 KiB626 linesraw
1--- @class Tape
2--- @field anchor MapPosition
3--- @field box BoundingBox
4--- @field cursor MapPosition
5--- @field drawing boolean
6--- @field editing boolean
7--- @field editing_box LuaEntity?
8--- @field move_drag_anchor MapPosition?
9--- @field tick_to_die MapTick
10--- @field settings TapeSettings
11--- @field id integer
12--- @field player LuaPlayer
13--- @field player_index uint
14--- @field surface LuaSurface
15--- @field background LuaRenderObject
16--- @field border LuaRenderObject
17--- @field label_north LuaRenderObject
18--- @field label_west LuaRenderObject
19--- @field lines LuaRenderObject[]
20
21--- @alias PlayerIndex uint32
22
23local flib_bounding_box = require("__flib__.bounding-box")
24local flib_position = require("__flib__.position")
25local flib_table = require("__flib__.table")
26
27--- @param player LuaPlayer
28--- @return LuaItemStack?
29local function get_tool(player)
30  local cursor_stack = player.cursor_stack
31  if cursor_stack and cursor_stack.valid_for_read and cursor_stack.name == "tl-tool" then
32    return cursor_stack
33  end
34end
35
36--- @param settings TapeSettings
37--- @param tape Tape?
38--- @return string
39local function make_tool_label(settings, tape)
40  local output = ""
41  if tape then
42    output = output
43      .. tostring(flib_bounding_box.width(tape.box))
44      .. "×"
45      .. tostring(flib_bounding_box.height(tape.box))
46      .. " | "
47  end
48  if settings.mode == "subgrid" then
49    return output .. "Subgrid mode | Size: " .. tostring(settings.subgrid_size)
50  else
51    return output .. "Split mode | # of splits: " .. tostring(settings.splits)
52  end
53end
54
55--- @param player LuaPlayer
56--- @param tape Tape?
57local function update_tool_label(player, tape)
58  local tool = get_tool(player)
59  if tool then
60    local settings = tape and tape.settings or storage.player_settings[player.index]
61    tool.label = make_tool_label(settings, tape)
62  end
63end
64
65
66--- @param player LuaPlayer
67--- @param position MapPosition
68--- @param include_temporary_tapes boolean
69local function get_tape_at_position(player, position, include_temporary_tapes)
70  --- @type Tape?
71  local result
72  for _, stored_tape in pairs(storage.tapes) do
73    if
74      (include_temporary_tapes or stored_tape.tick_to_die == math.huge)
75      and stored_tape.player == player
76      and stored_tape.surface == player.surface
77      and flib_bounding_box.contains_position(stored_tape.box, position)
78      and (not result or result.id < stored_tape.id)
79    then
80      result = stored_tape
81    end
82  end
83  return result
84end
85
86--- @param player LuaPlayer
87--- @param position MapPosition
88--- @param surface LuaSurface
89--- @return Tape
90local function new_tape(player, position, surface)
91  local id = storage.next_tape_id
92  storage.next_tape_id = id + 1
93  local box = flib_bounding_box.from_position(position, true)
94  local center = flib_bounding_box.center(box)
95  local width = flib_bounding_box.width(box)
96  local height = flib_bounding_box.height(box)
97
98  --- @type Tape
99  local self = {
100    anchor = position,
101    box = box,
102    cursor = position,
103    drawing = true,
104    editing = false,
105    id = id,
106    player = player,
107    player_index = player.index,
108    tick_to_die = math.huge,
109    settings = flib_table.deep_copy(storage.player_settings[player.index]),
110    surface = surface,
111    background = rendering.draw_rectangle({
112      color = player.mod_settings["tl-tape-background-color"].value --[[@as Color]],
113      filled = true,
114      players = { player },
115      surface = surface,
116      left_top = box.left_top,
117      right_bottom = box.right_bottom,
118      draw_on_ground = player.mod_settings["tl-draw-tape-on-ground"].value --[[@as boolean]],
119    }),
120    border = rendering.draw_rectangle({
121      color = player.mod_settings["tl-tape-border-color"].value --[[@as Color]],
122      filled = false,
123      width = player.mod_settings["tl-tape-line-width"].value --[[@as double]],
124      players = { player },
125      surface = surface,
126      left_top = box.left_top,
127      right_bottom = box.right_bottom,
128      draw_on_ground = player.mod_settings["tl-draw-tape-on-ground"].value --[[@as boolean]],
129    }),
130    label_north = rendering.draw_text({
131      text = tostring(width),
132      surface = surface,
133      target = { x = center.x, y = box.left_top.y },
134      color = player.mod_settings["tl-tape-label-color"].value --[[@as Color]],
135      scale = 2,
136      alignment = "center",
137      vertical_alignment = "bottom",
138      visible = width > 1,
139      players = { player },
140    }),
141    label_west = rendering.draw_text({
142      text = tostring(height),
143      surface = surface,
144      target = { x = box.left_top.x, y = center.y },
145      color = player.mod_settings["tl-tape-label-color"].value --[[@as Color]],
146      scale = 2,
147      alignment = "center",
148      vertical_alignment = "bottom",
149      orientation = 0.75,
150      visible = height > 1,
151      players = { player },
152    }),
153    lines = {},
154  }
155  storage.tapes[id] = self
156  update_tool_label(self.player, self)
157  return self
158end
159
160--- @param self Tape
161local function update_tape(self)
162  local line_width = self.player.mod_settings["tl-tape-line-width"].value --[[@as double]]
163
164  local box = self.box
165  local draw_on_ground = self.player.mod_settings["tl-draw-tape-on-ground"].value --[[@as boolean]]
166  self.background.color = self.player.mod_settings["tl-tape-background-color"].value --[[@as Color]]
167  self.background.left_top = box.left_top
168  self.background.right_bottom = box.right_bottom
169  self.background.draw_on_ground = draw_on_ground
170  self.border.color = self.player.mod_settings["tl-tape-border-color"].value --[[@as Color]]
171  self.border.left_top = box.left_top
172  self.border.right_bottom = box.right_bottom
173  self.border.draw_on_ground = draw_on_ground
174  self.border.width = line_width
175  local center = flib_bounding_box.center(box)
176  self.label_north.color = self.player.mod_settings["tl-tape-label-color"].value --[[@as Color]]
177  self.label_north.target = { x = center.x, y = box.left_top.y }
178  local width = flib_bounding_box.width(box)
179  self.label_north.text = tostring(width)
180  self.label_north.visible = width > 1
181  local height = flib_bounding_box.height(box)
182  self.label_west.color = self.player.mod_settings["tl-tape-label-color"].value --[[@as Color]]
183  self.label_west.target = { x = box.left_top.x, y = center.y }
184  self.label_west.text = height
185  self.label_west.visible = height > 1
186
187  local lines = self.lines
188  local i = 0
189
190  --- @param color Color
191  --- @param step_x integer
192  --- @param step_y integer?
193  local function draw_lines(color, step_x, step_y)
194    step_y = step_y or step_x
195    local from_x = self.anchor.x <= center.x and box.left_top.x or box.right_bottom.x
196    local from_y = self.anchor.y <= center.y and box.left_top.y or box.right_bottom.y
197    local to_x = self.anchor.x > center.x and box.left_top.x or box.right_bottom.x
198    local to_y = self.anchor.y > center.y and box.left_top.y or box.right_bottom.y
199
200    if flib_bounding_box.width(self.box) > 1 then
201      step_x = from_x <= to_x and step_x or -step_x
202      for x = from_x + step_x, to_x, step_x do
203        i = i + 1
204        local line = lines[i]
205        if line then
206          line.color = color
207          line.width = line_width
208          line.from = { x = x, y = from_y }
209          line.to = { x = x, y = to_y }
210          line.visible = true
211          line.draw_on_ground = draw_on_ground
212        else
213          line = rendering.draw_line({
214            color = color,
215            width = line_width,
216            from = { x = x, y = from_y },
217            to = { x = x, y = to_y },
218            surface = self.surface,
219            players = { self.player },
220            draw_on_ground = draw_on_ground,
221          })
222          lines[i] = line
223        end
224      end
225    end
226
227    if flib_bounding_box.height(self.box) > 1 then
228      step_y = from_y <= to_y and step_y or -step_y
229      for y = from_y + step_y, to_y, step_y do
230        i = i + 1
231        local line = lines[i]
232        if line then
233          line.color = color
234          line.width = line_width
235          line.from = { x = from_x, y = y }
236          line.to = { x = to_x, y = y }
237          line.visible = true
238          line.draw_on_ground = draw_on_ground
239        else
240          line = rendering.draw_line({
241            color = color,
242            width = line_width,
243            from = { x = from_x, y = y },
244            to = { x = to_x, y = y },
245            surface = self.surface,
246            players = { self.player },
247            draw_on_ground = draw_on_ground,
248          })
249          lines[i] = line
250        end
251      end
252    end
253  end
254
255  draw_lines(self.player.mod_settings["tl-tape-line-color-1"].value --[[@as Color]], 1)
256
257  if self.settings.mode == "subgrid" then
258    if self.settings.subgrid_size > 1 then
259      draw_lines(self.player.mod_settings["tl-tape-line-color-2"].value --[[@as Color]], self.settings.subgrid_size)
260      draw_lines(self.player.mod_settings["tl-tape-line-color-3"].value --[[@as Color]], self.settings.subgrid_size ^ 2)
261      draw_lines(self.player.mod_settings["tl-tape-line-color-4"].value --[[@as Color]], self.settings.subgrid_size ^ 3)
262    end
263  else
264    draw_lines(
265      self.player.mod_settings["tl-tape-line-color-2"].value --[[@as Color]],
266      flib_bounding_box.width(self.box) / self.settings.splits,
267      flib_bounding_box.height(self.box) / self.settings.splits
268    )
269    draw_lines(
270      self.player.mod_settings["tl-tape-line-color-3"].value --[[@as Color]],
271      flib_bounding_box.width(self.box) / 2,
272      flib_bounding_box.height(self.box) / 2
273    )
274  end
275
276  for j = i + 1, #lines do
277    lines[j].visible = false
278  end
279
280  self.border.bring_to_front()
281  if self.editing_box then
282    self.editing_box.destroy()
283  end
284  if self.editing then
285    self.editing_box = self.surface.create_entity({
286      name = "tl-highlight-box",
287      position = flib_bounding_box.center(self.box),
288      bounding_box = flib_bounding_box.resize(self.box, 0.3),
289      cursor_box_type = "electricity",
290      render_player_index = self.player_index,
291      blink_interval = 30,
292    })
293  end
294
295  update_tool_label(self.player, self)
296end
297
298--- @param self Tape
299--- @param position MapPosition
300--- @param constrained boolean
301local function resize_tape(self, position, constrained)
302  if constrained then
303    local delta = flib_position.abs(flib_position.sub(self.anchor, position))
304    if delta.x > delta.y then
305      position.y = self.anchor.y
306    else
307      position.x = self.anchor.x
308    end
309  end
310  local box = flib_bounding_box.from_position(self.anchor, true)
311  box = flib_bounding_box.expand_to_contain_position(box, position)
312  box = flib_bounding_box.ceil(box)
313  self.box = box
314  self.cursor = position
315  update_tape(self)
316end
317
318--- @param self Tape
319--- @param position MapPosition
320local function move_tape(self, position)
321  if not self.move_drag_anchor then
322    if flib_bounding_box.contains_position(self.box, position) then
323      self.move_drag_anchor = position
324    end
325    return
326  end
327  local delta = flib_position.sub(position, self.move_drag_anchor --[[@as MapPosition]])
328  self.anchor = flib_position.add(self.anchor, delta)
329  self.box = flib_bounding_box.move(self.box, delta)
330  self.move_drag_anchor = position
331  update_tape(self)
332end
333
334--- @param self Tape
335local function destroy_tape(self)
336  if self.background.valid then
337    self.background.destroy()
338  end
339  if self.border.valid then
340    self.border.destroy()
341  end
342  if self.label_north.valid then
343    self.label_north.destroy()
344  end
345  if self.label_west.valid then
346    self.label_west.destroy()
347  end
348  for _, line in pairs(self.lines) do
349    if line.valid then
350      line.destroy()
351    end
352  end
353
354  if self.editing_box and self.editing_box.valid then
355    self.editing_box.destroy()
356  end
357
358  if self.editing then
359    storage.editing[self.player_index] = nil
360  elseif self.drawing then
361    storage.drawing[self.player_index] = nil
362  else
363    storage.tapes[self.id] = nil
364  end
365end
366
367local allowed_selection_modes = {
368  [defines.selection_mode.select] = true,
369  [defines.selection_mode.alt_select] = true,
370}
371
372--- @param e EventData.on_player_selected_area
373local function on_player_live_selected_area(e)
374  if e.item ~= "tl-tool" or allowed_selection_modes[e.selection_mode] == nil then
375    return
376  end
377
378  -- TODO: Consider refactoring to just use e.area instead of constructing the area from positions.
379  local position = flib_position.floor(e.cursor_position)
380  local surface = e.surface
381  local last_position = storage.last_position[e.player_index]
382  if last_position and flib_position.eq(position, last_position.position) and surface == last_position.surface then
383    return
384  end
385  storage.last_position[e.player_index] = { position = position, surface = surface }
386
387  local should_cancel = last_position and surface ~= last_position.surface
388
389  local editing = storage.editing[e.player_index]
390  if editing then
391    if should_cancel then
392      destroy_tape(editing)
393      return
394    end
395    move_tape(editing, position)
396    return
397  end
398  local drawing = storage.drawing[e.player_index]
399  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
400  if drawing then
401    if should_cancel then
402      destroy_tape(drawing)
403      return
404    end
405    resize_tape(drawing, position, e.selection_mode == defines.selection_mode.select)
406  else
407    drawing = new_tape(player, position, surface)
408    storage.drawing[e.player_index] = drawing
409  end
410end
411
412--- @param e EventData.on_player_selected_area|EventData.on_player_alt_selected_area
413local function on_player_selected_area(e)
414  local editing_tape = storage.editing[e.player_index]
415  if editing_tape then
416    editing_tape.move_drag_anchor = nil
417    return
418  end
419  local drawing_tape = storage.drawing[e.player_index]
420  if not drawing_tape then
421    return
422  end
423  storage.drawing[e.player_index] = nil
424  drawing_tape.drawing = false
425  if e.name == defines.events.on_player_selected_area then
426    local time_to_live = drawing_tape.player.mod_settings["tl-tape-clear-delay"].value --[[@as double]]
427    drawing_tape.tick_to_die = game.tick + time_to_live * 60
428  end
429  storage.tapes[drawing_tape.id] = drawing_tape
430  local player = game.get_player(e.player_index) --- @as LuaPlayer
431  update_tool_label(player)
432end
433
434--- @param e EventData.on_tick
435local function on_tick(e)
436  for _, tape_data in pairs(storage.tapes or {}) do
437    if tape_data.tick_to_die > e.tick then
438      goto continue
439    end
440
441    destroy_tape(tape_data)
442
443    ::continue::
444  end
445end
446
447--- @param e EventData.CustomInputEvent
448local function on_edit_tape(e)
449  if storage.editing[e.player_index] then
450    return
451  end
452  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
453  if not get_tool(player) then
454    return
455  end
456  local tape = get_tape_at_position(player, e.cursor_position, false)
457  if not tape then
458    return
459  end
460  tape.editing = true
461  storage.editing[e.player_index] = tape
462  storage.tapes[tape.id] = nil
463  update_tape(tape)
464end
465
466--- @param e EventData.CustomInputEvent
467local function on_delete_tape(e)
468  if storage.drawing[e.player_index] then
469    return
470  end
471
472  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
473  if not get_tool(player) then
474    return
475  end
476
477  local editing = storage.editing[e.player_index]
478  if editing and flib_bounding_box.contains_position(editing.box, e.cursor_position) then
479    destroy_tape(editing)
480    return
481  end
482
483  local tape = get_tape_at_position(player, e.cursor_position, true)
484  if tape then
485    destroy_tape(tape)
486  end
487end
488
489--- @param e EventData.CustomInputEvent
490local function on_clear_cursor(e)
491  local tape = storage.editing[e.player_index]
492  if not tape then
493    return
494  end
495  tape.editing = false
496  storage.editing[e.player_index] = nil
497  storage.tapes[tape.id] = tape
498  update_tape(tape)
499end
500
501--- @param e EventData.CustomInputEvent
502local function on_change_mode(e)
503  local player = game.get_player(e.player_index) --- @as LuaPlayer
504  if not get_tool(player) then
505    return
506  end
507  local tape = storage.editing[e.player_index]
508  if tape then
509    tape.settings.mode = tape.settings.mode == "subgrid" and "split" or "subgrid"
510    update_tape(tape)
511    return
512  end
513  local settings = storage.player_settings[e.player_index]
514  settings.mode = settings.mode == "subgrid" and "split" or "subgrid"
515  local player = game.get_player(e.player_index)
516  --- @cast player -?
517  update_tool_label(player)
518end
519
520--- @param e EventData.CustomInputEvent
521local function on_change_divisor(e)
522  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
523  if not get_tool(player) then
524    return
525  end
526
527  local delta = e.input_name == "tl-increase-divisor" and 1 or -1
528  local tape = storage.editing[e.player_index]
529  local settings = tape and tape.settings or storage.player_settings[e.player_index]
530  if not settings then
531    return
532  end
533  if settings.mode == "subgrid" then
534    settings.subgrid_size = math.max(1, settings.subgrid_size + delta)
535  else
536    settings.splits = math.max(2, settings.splits + delta)
537  end
538  if tape then
539    update_tape(tape)
540  else
541    update_tool_label(player)
542  end
543end
544
545--- @param e EventData.on_player_cursor_stack_changed
546local function on_player_cursor_stack_changed(e)
547  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
548  update_tool_label(player)
549end
550
551--- @param e EventData.on_runtime_mod_setting_changed
552local function on_runtime_mod_setting_changed(e)
553  if not e.player_index then
554    return
555  end
556  if not string.find(e.setting, "^tl%-") then
557    return
558  end
559  local editing = storage.editing[e.player_index]
560  if editing then
561    update_tape(editing)
562  end
563  local drawing = storage.drawing[e.player_index]
564  if drawing then
565    update_tape(drawing)
566  end
567  for _, tape in pairs(storage.tapes) do
568    if tape.player.index == e.player_index then
569      update_tape(tape)
570    end
571  end
572end
573
574--- @param e EventData.on_player_removed
575local function on_player_removed(e)
576  local editing = storage.editing[e.player_index]
577  if editing then
578    destroy_tape(editing)
579  end
580  local drawing = storage.drawing[e.player_index]
581  if drawing then
582    destroy_tape(drawing)
583  end
584  for _, tape in pairs(storage.tapes) do
585    if tape.player_index == e.player_index then
586      destroy_tape(tape)
587    end
588  end
589end
590
591local tape = {}
592
593function tape.on_init()
594  --- @type table<PlayerIndex, Tape>
595  storage.editing = {}
596  --- @type table<PlayerIndex, Tape>
597  storage.drawing = {}
598  --- @type table<PlayerIndex, {position: MapPosition, surface: LuaSurface}>
599  storage.last_position = {}
600  storage.next_tape_id = 1
601  --- @type table<uint, Tape>
602  storage.tapes = {}
603end
604
605tape.events = {
606  [defines.events.on_player_start_selected_area] = on_player_live_selected_area,
607  [defines.events.on_player_change_selected_area] = on_player_live_selected_area,
608  -- [defines.events.on_player_cancel_selected_area] = on_player_cancel_selected_area,
609  [defines.events.on_player_alt_selected_area] = on_player_selected_area,
610  [defines.events.on_player_selected_area] = on_player_selected_area,
611  [defines.events.on_player_cursor_stack_changed] = on_player_cursor_stack_changed,
612  [defines.events.on_tick] = on_tick,
613  ["tl-delete-tape"] = on_delete_tape,
614  ["tl-edit-tape"] = on_edit_tape,
615  ["tl-linked-clear-cursor"] = on_clear_cursor,
616  ["tl-next-mode"] = on_change_mode,
617  ["tl-previous-mode"] = on_change_mode,
618  ["tl-increase-divisor"] = on_change_divisor,
619  ["tl-decrease-divisor"] = on_change_divisor,
620  [defines.events.on_runtime_mod_setting_changed] = on_runtime_mod_setting_changed,
621  [defines.events.on_player_removed] = on_player_removed,
622}
623
624tape.get_tool = get_tool
625
626return tape