z_mirror/EditorExtensions

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

Caleb HeuerFix pipe snapping891dd4a

1.14
22.0 KiB721 linesraw
1local format = require("__flib__/format")
2local gui = require("__flib__/gui-lite")
3local math = require("__flib__/math")
4local table = require("__flib__/table")
5
6local shared_constants = require("__EditorExtensions__/shared-constants")
7
8local util = require("__EditorExtensions__/scripts/util")
9
10local crafter_snapping_types = {
11  ["assembling-machine"] = true,
12  ["furnace"] = true,
13  ["rocket-silo"] = true,
14}
15
16--- @alias InfinityPipeMode
17--- | "at-least",
18--- | "at-most",
19--- | "exactly",
20--- | "add",
21--- | "remove",
22
23--- @type table<string, uint>
24local defines_amount_type = {
25  percent = 1,
26  units = 2,
27}
28
29--- @param entity LuaEntity
30--- @param amount_type uint?
31local function store_amount_type(entity, amount_type)
32  global.infinity_pipe_amount_type[entity.unit_number] = amount_type
33end
34
35--- @param entity LuaEntity
36--- @return uint?
37local function get_stored_amount_type(entity)
38  return global.infinity_pipe_amount_type[entity.unit_number]
39end
40
41--- @param entity LuaEntity
42--- @return uint?
43local function remove_stored_amount_type(entity)
44  local value = global.infinity_pipe_amount_type[entity.unit_number]
45  global.infinity_pipe_amount_type[entity.unit_number] = nil
46  return value
47end
48
49--- @param entity LuaEntity
50--- @param new_capacity double
51--- @return LuaEntity?
52local function swap_entity(entity, new_capacity)
53  local amount_type = remove_stored_amount_type(entity)
54  local new_entity = entity.surface.create_entity({
55    name = "ee-infinity-pipe-" .. new_capacity,
56    position = entity.position,
57    direction = entity.direction,
58    force = entity.force,
59    fast_replace = true,
60    create_build_effect_smoke = false,
61    spill = false,
62  })
63  if not new_entity then
64    return
65  end
66  store_amount_type(new_entity, amount_type)
67  return new_entity
68end
69
70--- @param entity BlueprintEntity|LuaEntity
71local function check_is_our_pipe(entity)
72  return string.find(entity.name, "^ee%-infinity%-pipe%-%d+$")
73end
74
75--- @param entity LuaEntity
76local function snap(entity)
77  local own_id = entity.unit_number
78  for _, fluidbox in pairs(entity.fluidbox.get_connections(1)) do
79    if crafter_snapping_types[fluidbox.owner.type] then
80      for i = 1, #fluidbox do
81        --- @cast i uint
82        local connections = fluidbox.get_connections(i)
83        local prototype = fluidbox.get_prototype(i)
84        for j = 1, #connections do
85          if prototype.production_type == "input" and connections[j].owner.unit_number == own_id then
86            local fluid = fluidbox.get_locked_fluid(i)
87            if fluid then
88              entity.set_infinity_pipe_filter({ name = fluid, percentage = 1, mode = "at-least" })
89              return
90            end
91          end
92        end
93      end
94    end
95  end
96end
97
98--- @param filter InfinityPipeFilter?
99--- @return double, double
100local function get_temperature_limits(filter)
101  if filter and filter.name then
102    local prototype = game.fluid_prototypes[filter.name]
103    return prototype.default_temperature, prototype.max_temperature
104  end
105  return 0, 100
106end
107
108-- GUI
109
110--- @class InfinityPipeGui
111--- @field elems InfinityPipeGuiElems
112--- @field entity LuaEntity
113--- @field mode InfinityPipeMode
114--- @field player LuaPlayer
115--- @field temperature double
116
117--- @class InfinityPipeGuiElems
118--- @field ee_infinity_pipe_window LuaGuiElement
119--- @field titlebar_flow LuaGuiElement
120--- @field drag_handle LuaGuiElement
121--- @field capacity_dropdown LuaGuiElement
122--- @field amount_progressbar LuaGuiElement
123--- @field entity_preview LuaGuiElement
124--- @field filter_button LuaGuiElement
125--- @field amount_slider LuaGuiElement
126--- @field amount_textfield LuaGuiElement
127--- @field amount_type_dropdown LuaGuiElement
128--- @field mode_radio_button_at_least LuaGuiElement
129--- @field mode_radio_button_at_most LuaGuiElement
130--- @field mode_radio_button_exactly LuaGuiElement
131--- @field mode_radio_button_add LuaGuiElement
132--- @field mode_radio_button_remove LuaGuiElement
133--- @field temperature_slider LuaGuiElement
134--- @field temperature_textfield LuaGuiElement
135
136--- @param player_index uint
137local function destroy_gui(player_index)
138  local self = global.infinity_pipe_gui[player_index]
139  if not self then
140    return
141  end
142  global.infinity_pipe_gui[player_index] = nil
143  local window = self.elems.ee_infinity_pipe_window
144  if window.valid then
145    window.destroy()
146  end
147end
148
149--- @param self InfinityPipeGui
150local function update_fluid_content_bar(self)
151  local bar = self.elems.amount_progressbar
152  local fluidbox = self.entity.fluidbox
153  local capacity = fluidbox.get_capacity(1)
154  local content = fluidbox[1] or { amount = 0 }
155
156  local caption = format.number(content.amount) .. " / " .. format.number(capacity)
157  bar.caption = caption
158  bar.value = content.amount / capacity
159
160  if not content.name then
161    return
162  end
163
164  local bar_color = game.fluid_prototypes[content.name].base_color
165  -- Calculate luminance of the background color
166  -- Source: https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color
167  local luminance = (0.2126 * bar_color.r) + (0.7152 * bar_color.g) + (0.0722 * bar_color.b)
168  if luminance > 0.55 then
169    bar.style = "ee_infinity_pipe_progressbar"
170  else
171    bar.style = "ee_infinity_pipe_progressbar_light_text"
172  end
173  bar.style.color = bar_color
174end
175
176--- @param self InfinityPipeGui
177--- @param new_entity LuaEntity?
178--- @param reset_temperature boolean?
179local function update_gui(self, new_entity, reset_temperature)
180  if not new_entity and not self.entity.valid then
181    destroy_gui(self.player.index)
182    return
183  end
184  local elems = self.elems
185  if new_entity then
186    elems.entity_preview.entity = new_entity
187    self.entity = new_entity
188  end
189  local entity = self.entity
190  local filter = entity.get_infinity_pipe_filter() or {}
191  if filter.mode then
192    self.mode = filter.mode
193  end
194  if filter.temperature then
195    self.temperature = filter.temperature
196  elseif reset_temperature then
197    self.temperature = 0
198  end
199
200  local capacity = entity.fluidbox.get_capacity(1)
201  elems.capacity_dropdown.selected_index = table.find(shared_constants.infinity_pipe_capacities, capacity) --[[@as uint]]
202
203  elems.filter_button.elem_value = filter.name
204
205  local amount_type = get_stored_amount_type(entity) or 1
206  elems.amount_slider.slider_value = filter.percentage or 0
207  elems.amount_textfield.style = "ee_slider_textfield"
208  local percentage = filter.percentage or 0
209  if amount_type == defines_amount_type.percent then
210    elems.amount_textfield.text = tostring(math.floor(percentage * 100))
211  else
212    elems.amount_textfield.text = tostring(math.floor(percentage * capacity))
213  end
214  elems.amount_type_dropdown.selected_index = amount_type
215
216  local mode = self.mode
217  elems.mode_radio_button_at_least.state = mode == "at-least"
218  elems.mode_radio_button_at_most.state = mode == "at-most"
219  elems.mode_radio_button_exactly.state = mode == "exactly"
220  elems.mode_radio_button_add.state = mode == "add"
221  elems.mode_radio_button_remove.state = mode == "remove"
222
223  local min, max = get_temperature_limits(filter)
224  if min == max then
225    elems.temperature_slider.enabled = false
226    elems.temperature_textfield.enabled = false
227    elems.temperature_slider.set_slider_minimum_maximum(min, max + 1)
228    elems.temperature_slider.slider_value = min
229  else
230    elems.temperature_slider.enabled = true
231    elems.temperature_textfield.enabled = true
232    elems.temperature_slider.set_slider_minimum_maximum(min, max)
233    elems.temperature_slider.slider_value = self.temperature
234  end
235  elems.temperature_textfield.style = "ee_slider_textfield"
236  elems.temperature_textfield.text = tostring(self.temperature)
237
238  update_fluid_content_bar(self)
239end
240
241--- @param entity LuaEntity
242--- @param reset_temperature boolean?
243local function update_all_guis(entity, reset_temperature)
244  for _, gui in pairs(global.infinity_pipe_gui) do
245    if not gui.entity.valid then
246      update_gui(gui, entity, reset_temperature)
247    elseif gui.entity == entity then
248      update_gui(gui, nil, reset_temperature)
249    end
250  end
251end
252
253local handlers = {
254  --- @param self InfinityPipeGui
255  --- @param e EventData.on_gui_closed|EventData.on_gui_click
256  on_ip_gui_closed = function(self, e)
257    destroy_gui(e.player_index)
258    local player = self.player
259    if not player.valid then
260      return
261    end
262    player.play_sound({ path = "entity-close/ee-infinity-accumulator-tertiary-buffer" })
263  end,
264
265  --- @param self InfinityPipeGui
266  --- @param e EventData.on_gui_selection_state_changed
267  on_ip_gui_capacity_dropdown_selection_state_changed = function(self, e)
268    local entity = self.entity
269    local new_capacity = shared_constants.infinity_pipe_capacities[e.element.selected_index]
270    if new_capacity == entity.fluidbox.get_capacity(1) then
271      return
272    end
273    local new_entity = swap_entity(entity, new_capacity)
274    if not new_entity then
275      return
276    end
277    update_all_guis(new_entity)
278  end,
279
280  --- @param self InfinityPipeGui
281  --- @param e EventData.on_gui_elem_changed
282  on_ip_gui_filter_changed = function(self, e)
283    local entity = self.entity
284    local elem = e.element.elem_value --[[@as string?]]
285    if elem then
286      if not filter then
287        filter = { mode = self.mode, percentage = 1 }
288      end
289      filter.name = elem
290      entity.set_infinity_pipe_filter(filter)
291    else
292      entity.set_infinity_pipe_filter(nil)
293    end
294    update_all_guis(entity, not elem)
295  end,
296
297  --- @param self InfinityPipeGui
298  --- @param e EventData.on_gui_elem_changed
299  on_ip_gui_amount_slider_value_changed = function(self, e)
300    local entity = self.entity
301    local value = e.element.slider_value
302    local filter = entity.get_infinity_pipe_filter()
303    if not filter then
304      -- Textfield should always match slider
305      local textfield = self.elems.amount_textfield
306      textfield.style = "ee_slider_textfield"
307      textfield.text = tostring(math.floor(value * 100))
308      return
309    end
310    filter.percentage = value
311    entity.set_infinity_pipe_filter(filter)
312    update_all_guis(entity)
313  end,
314
315  --- @param self InfinityPipeGui
316  --- @param e EventData.on_gui_text_changed
317  on_ip_gui_amount_textfield_changed = function(self, e)
318    local entity = self.entity
319    local textfield = e.element
320    local text = textfield.text
321    local value = tonumber(text)
322    local max = 100
323    local amount_type = get_stored_amount_type(entity)
324    if amount_type == defines_amount_type.units then
325      max = entity.fluidbox.get_capacity(1)
326    end
327    if not value or value < 0 or value > max then
328      textfield.style = "ee_invalid_slider_textfield"
329      return
330    end
331    textfield.style = "ee_slider_textfield"
332
333    local percentage = value / 100
334    if amount_type == defines_amount_type.units then
335      local capacity = entity.fluidbox.get_capacity(1)
336      percentage = value / capacity
337    end
338
339    self.elems.amount_slider.slider_value = percentage
340
341    local filter = entity.get_infinity_pipe_filter()
342    if filter then
343      filter.percentage = percentage
344      entity.set_infinity_pipe_filter(filter)
345      update_all_guis(entity)
346    end
347  end,
348
349  --- @param self InfinityPipeGui
350  --- @param e EventData.on_gui_selection_state_changed
351  on_ip_gui_amount_type_changed = function(self, e)
352    local amount_type = e.element.selected_index
353    store_amount_type(self.entity, amount_type)
354    update_all_guis(self.entity)
355  end,
356
357  --- @param self InfinityPipeGui
358  --- @param e EventData.on_gui_checked_state_changed
359  on_ip_gui_mode_radio_button_selected = function(self, e)
360    local mode = e.element.tags.mode --[[@as string]]
361    self.mode = mode
362
363    local entity = self.entity
364    local filter = entity.get_infinity_pipe_filter()
365    if filter then
366      filter.mode = mode
367      entity.set_infinity_pipe_filter(filter)
368    end
369
370    update_all_guis(entity)
371  end,
372
373  --- @param self InfinityPipeGui
374  --- @param e EventData.on_gui_value_changed
375  on_ip_gui_temperature_slider_value_changed = function(self, e)
376    local entity = self.entity
377    local value = math.floor(e.element.slider_value)
378    local filter = entity.get_infinity_pipe_filter()
379    if not filter then
380      -- Textfield should always match slider
381      local textfield = self.elems.temperature_textfield
382      textfield.style = "ee_slider_textfield"
383      textfield.text = tostring(value)
384      return
385    end
386    filter.temperature = value
387    entity.set_infinity_pipe_filter(filter)
388    update_all_guis(entity)
389  end,
390
391  --- @param self InfinityPipeGui
392  --- @param e EventData.on_gui_text_changed
393  on_ip_gui_temperature_textfield_changed = function(self, e)
394    local entity = self.entity
395    local textfield = e.element
396    local text = textfield.text
397    local value = tonumber(text)
398
399    local filter = entity.get_infinity_pipe_filter()
400    local min, max = get_temperature_limits(filter)
401
402    if not value or value < min or value > max then
403      textfield.style = "ee_invalid_slider_textfield"
404      return
405    end
406    textfield.style = "ee_slider_textfield"
407
408    self.elems.temperature_slider.slider_value = value
409
410    if filter then
411      filter.temperature = value
412      entity.set_infinity_pipe_filter(filter)
413      update_all_guis(entity)
414    end
415  end,
416}
417
418gui.add_handlers(handlers, function(e, handler)
419  local self = global.infinity_pipe_gui[e.player_index]
420  if not self then
421    return
422  end
423  if not self.entity.valid then
424    return
425  end
426  handler(self, e)
427end)
428
429--- @param player LuaPlayer
430--- @param entity LuaEntity
431local function create_gui(player, entity)
432  destroy_gui(player.index)
433
434  --- @type InfinityPipeGuiElems
435  local elems = gui.add(player.gui.screen, {
436    type = "frame",
437    name = "ee_infinity_pipe_window",
438    direction = "vertical",
439    elem_mods = { auto_center = true },
440    handler = { [defines.events.on_gui_closed] = handlers.on_ip_gui_closed },
441    {
442      type = "flow",
443      name = "titlebar_flow",
444      style = "flib_titlebar_flow",
445      drag_target = "ee_infinity_pipe_window",
446      {
447        type = "label",
448        style = "frame_title",
449        caption = { "entity-name.ee-infinity-pipe" },
450        ignored_by_interaction = true,
451      },
452      { type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
453      util.close_button(handlers.on_ip_gui_closed),
454    },
455    {
456      type = "frame",
457      style = "entity_frame",
458      direction = "vertical",
459      {
460        type = "frame",
461        style = "deep_frame_in_shallow_frame",
462        style_mods = { bottom_margin = 4 },
463        {
464          type = "entity-preview",
465          name = "entity_preview",
466          style = "wide_entity_button",
467          elem_mods = { entity = entity },
468        },
469      },
470      {
471        type = "flow",
472        style_mods = { horizontal_spacing = 8, vertical_align = "center" },
473        {
474          type = "progressbar",
475          name = "amount_progressbar",
476          style = "ee_infinity_pipe_progressbar",
477        },
478        { type = "label", caption = "/" },
479        {
480          type = "drop-down",
481          name = "capacity_dropdown",
482          items = table.map(shared_constants.infinity_pipe_capacities, function(capacity)
483            return format.number(capacity)
484          end),
485          selected_index = 0,
486          handler = {
487            [defines.events.on_gui_selection_state_changed] = handlers.on_ip_gui_capacity_dropdown_selection_state_changed,
488          },
489        },
490      },
491      { type = "line", direction = "horizontal" },
492      {
493        type = "flow",
494        style = "centering_horizontal_flow",
495        direction = "horizontal",
496        {
497          type = "choose-elem-button",
498          name = "filter_button",
499          style = "flib_standalone_slot_button_default",
500          elem_type = "fluid",
501          handler = { [defines.events.on_gui_elem_changed] = handlers.on_ip_gui_filter_changed },
502        },
503        {
504          type = "slider",
505          name = "amount_slider",
506          style_mods = { horizontally_stretchable = true, margin = { 0, 8, 0, 8 } },
507          minimum_value = 0,
508          maximum_value = 1,
509          value_step = 0.01,
510          value = 0,
511          handler = { [defines.events.on_gui_value_changed] = handlers.on_ip_gui_amount_slider_value_changed },
512        },
513        {
514          type = "textfield",
515          name = "amount_textfield",
516          style = "slider_value_textfield",
517          numeric = true,
518          allow_decimal = true,
519          clear_and_focus_on_right_click = true,
520          text = "0",
521          handler = { [defines.events.on_gui_text_changed] = handlers.on_ip_gui_amount_textfield_changed },
522        },
523        {
524          type = "drop-down",
525          name = "amount_type_dropdown",
526          style_mods = { width = 55 },
527          items = { { "gui-infinity-pipe.percent" }, { "gui-infinity-pipe.ee-units" } },
528          selected_index = 1,
529          handler = { [defines.events.on_gui_selection_state_changed] = handlers.on_ip_gui_amount_type_changed },
530        },
531      },
532      {
533        type = "flow",
534        style_mods = { horizontal_spacing = 0 },
535        direction = "horizontal",
536        util.mode_radio_button("at-least", handlers.on_ip_gui_mode_radio_button_selected),
537        util.pusher(),
538        util.mode_radio_button("at-most", handlers.on_ip_gui_mode_radio_button_selected),
539        util.pusher(),
540        util.mode_radio_button("exactly", handlers.on_ip_gui_mode_radio_button_selected),
541        util.pusher(),
542        util.mode_radio_button("add", handlers.on_ip_gui_mode_radio_button_selected),
543        util.pusher(),
544        util.mode_radio_button("remove", handlers.on_ip_gui_mode_radio_button_selected),
545      },
546      { type = "line", direction = "horizontal" },
547      {
548        type = "flow",
549        style = "centering_horizontal_flow",
550        direction = "horizontal",
551        { type = "label", caption = { "gui-infinity-pipe.temperature" } },
552        {
553          type = "slider",
554          name = "temperature_slider",
555          style_mods = { horizontally_stretchable = true, margin = { 0, 8, 0, 8 } },
556          minimum_value = 0,
557          maximum_value = 100,
558          value = 0,
559          handler = {
560            [defines.events.on_gui_value_changed] = handlers.on_ip_gui_temperature_slider_value_changed,
561          },
562        },
563        {
564          type = "textfield",
565          name = "temperature_textfield",
566          style = "slider_value_textfield",
567          clear_and_focus_on_right_click = true,
568          text = "0",
569          handler = {
570            [defines.events.on_gui_text_changed] = handlers.on_ip_gui_temperature_textfield_changed,
571          },
572        },
573      },
574    },
575  })
576
577  player.opened = elems.ee_infinity_pipe_window
578
579  --- @type InfinityPipeGui
580  local self = {
581    elems = elems,
582    entity = entity,
583    mode = "at-least",
584    temperature = 0,
585    player = player,
586  }
587  global.infinity_pipe_gui[player.index] = self
588
589  update_gui(self)
590end
591
592--- @param e BuiltEvent
593local function on_entity_built(e)
594  local entity = e.created_entity or e.entity or e.destination
595  if not entity or not entity.valid or not check_is_our_pipe(entity) then
596    return
597  end
598
599  local tags = e.tags
600  if tags and tags.EditorExtensions then
601    store_amount_type(entity, tags.EditorExtensions.amount_type --[[@as uint]])
602  end
603
604  if e.name == defines.events.on_built_entity then
605    snap(entity)
606  end
607end
608
609--- @param e DestroyedEvent
610local function on_entity_destroyed(e)
611  local entity = e.entity
612  if not entity.valid or not check_is_our_pipe(entity) then
613    return
614  end
615  remove_stored_amount_type(e.entity)
616end
617
618--- @param e EventData.on_gui_opened
619local function on_gui_opened(e)
620  local entity = e.entity
621  if not entity or not entity.valid or not check_is_our_pipe(entity) then
622    return
623  end
624  local player = game.get_player(e.player_index)
625  if not player then
626    return
627  end
628  create_gui(player, entity)
629end
630
631--- @param e EventData.on_player_setup_blueprint
632local function on_player_setup_blueprint(e)
633  local blueprint = util.get_blueprint(e)
634  if not blueprint then
635    return
636  end
637
638  local entities = blueprint.get_blueprint_entities()
639  if not entities then
640    return
641  end
642  for i, entity in pairs(entities) do
643    --- @cast i uint
644    if not check_is_our_pipe(entity) then
645      goto continue
646    end
647    local real_entity = e.surface.find_entity(entity.name, entity.position)
648    if not real_entity then
649      goto continue
650    end
651    blueprint.set_blueprint_entity_tag(
652      i,
653      "EditorExtensions",
654      { amount_type = global.infinity_pipe_amount_type[real_entity.unit_number] }
655    )
656    ::continue::
657  end
658end
659
660--- @param e EventData.on_entity_settings_pasted
661local function on_entity_settings_pasted(e)
662  local source, destination = e.source, e.destination
663  if not check_is_our_pipe(source) or not check_is_our_pipe(destination) then
664    return
665  end
666
667  if source.name ~= destination.name then
668    local new_destination = swap_entity(destination, source.fluidbox.get_capacity(1))
669    if not new_destination then
670      return
671    end
672    destination = new_destination
673  end
674
675  local source_amount_type = get_stored_amount_type(source)
676  local destination_amount_type = get_stored_amount_type(destination)
677  if source_amount_type ~= destination_amount_type then
678    store_amount_type(destination, source_amount_type)
679  end
680
681  update_all_guis(destination)
682end
683
684local infinity_pipe = {}
685
686infinity_pipe.on_init = function()
687  global.infinity_pipe_gui = {}
688  --- @type table<uint, uint>
689  global.infinity_pipe_amount_type = {}
690end
691
692infinity_pipe.on_configuration_changed = function()
693  for player_index in pairs(game.players) do
694    destroy_gui(player_index --[[@as uint]])
695  end
696end
697
698infinity_pipe.events = {
699  [defines.events.on_built_entity] = on_entity_built,
700  [defines.events.on_entity_cloned] = on_entity_built,
701  [defines.events.on_entity_died] = on_entity_destroyed,
702  [defines.events.on_entity_settings_pasted] = on_entity_settings_pasted,
703  [defines.events.on_gui_opened] = on_gui_opened,
704  [defines.events.on_player_mined_entity] = on_entity_destroyed,
705  [defines.events.on_player_setup_blueprint] = on_player_setup_blueprint,
706  [defines.events.on_robot_built_entity] = on_entity_built,
707  [defines.events.on_robot_mined_entity] = on_entity_destroyed,
708  [defines.events.script_raised_built] = on_entity_built,
709  [defines.events.script_raised_destroy] = on_entity_destroyed,
710  [defines.events.script_raised_revive] = on_entity_built,
711}
712
713infinity_pipe.on_nth_tick = {
714  [1] = function()
715    for _, gui in pairs(global.infinity_pipe_gui) do
716      update_fluid_content_bar(gui)
717    end
718  end,
719}
720
721return infinity_pipe