z_mirror/RecipeBook

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

Caleb HeuerRemove dead code6169e8f

trunk
16.3 KiB604 linesraw
1local flib_dictionary = require("__flib__.dictionary")
2local flib_gui = require("__flib__.gui")
3
4local grouped = require("scripts.database.grouped")
5local gui_util = require("scripts.gui.util")
6local history = require("scripts.gui.history")
7local info_pane = require("scripts.gui.info-pane")
8local search_pane = require("scripts.gui.search-pane")
9local util = require("scripts.util")
10
11--- @param name string
12--- @param sprite string
13--- @param tooltip LocalisedString
14--- @param handler flib.GuiElemHandler
15--- @param auto_toggle boolean
16--- @return LuaGuiElement.add_param
17local function frame_action_button(name, sprite, tooltip, handler, auto_toggle)
18  return {
19    type = "sprite-button",
20    name = name,
21    style = "frame_action_button",
22    sprite = sprite,
23    tooltip = tooltip,
24    mouse_button_filter = { "left" },
25    tags = flib_gui.format_handlers({ [defines.events.on_gui_click] = handler }),
26    auto_toggle = auto_toggle,
27  }
28end
29
30--- @class MainGuiContext
31--- @field grouping_mode GroupingMode
32--- @field search_materials_only boolean
33--- @field show_hidden boolean
34--- @field show_unresearched boolean
35--- @field player LuaPlayer
36
37--- @class MainGui
38--- @field context MainGuiContext
39--- @field window LuaGuiElement
40--- @field header LuaGuiElement
41--- @field search_pane SearchPane
42--- @field info_pane InfoPane
43--- @field history History
44--- @field pinned boolean
45--- @field update_when_opened boolean
46local main_gui = {}
47local mt = { __index = main_gui }
48script.register_metatable("main_gui", mt)
49
50--- @param player LuaPlayer
51--- @return MainGui
52function main_gui.build(player)
53  main_gui.destroy(player.index)
54
55  --- @type MainGuiContext
56  local context = {
57    grouping_mode = player.mod_settings["rb-grouping-mode"].value --[[@as string]],
58    search_materials_only = player.mod_settings["rb-search-materials-only"].value --[[@as boolean]],
59    show_hidden = false,
60    show_unresearched = true,
61    player = player,
62  }
63
64  local window = player.gui.screen.add({
65    type = "frame",
66    name = "rb_main_window",
67    direction = "vertical",
68    visible = false,
69    tags = flib_gui.format_handlers({ [defines.events.on_gui_closed] = main_gui.on_window_closed }),
70  })
71  window.auto_center = true
72
73  local header = window.add({
74    type = "flow",
75    style = "flib_titlebar_flow",
76    tags = flib_gui.format_handlers({ [defines.events.on_gui_click] = main_gui.on_titlebar_clicked }),
77  })
78  header.drag_target = window
79  header.add({
80    type = "label",
81    style = "flib_frame_title",
82    caption = { "gui.rb-title" },
83    ignored_by_interaction = true,
84  })
85  header.add({ type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true })
86  header.add(
87    frame_action_button(
88      "show_unresearched_button",
89      "rb_show_unresearched",
90      { "gui.rb-show-unresearched-instruction" },
91      main_gui.on_show_unresearched_button_clicked,
92      true
93    )
94  )
95  header.add(
96    frame_action_button(
97      "show_hidden_button",
98      "rb_show_hidden",
99      { "gui.rb-show-hidden-instruction" },
100      main_gui.on_show_hidden_button_clicked,
101      true
102    )
103  )
104  header.show_hidden_button.visible = util.show_hidden_enabled
105  header.add(
106    frame_action_button(
107      "nav_backward_button",
108      "flib_nav_backward_white",
109      { "gui.rb-nav-backward-instruction" },
110      main_gui.prev,
111      false
112    )
113  )
114  header.add(
115    frame_action_button(
116      "nav_forward_button",
117      "flib_nav_forward_white",
118      { "gui.rb-nav-forward-instruction" },
119      main_gui.next,
120      false
121    )
122  )
123  header.add(
124    frame_action_button("pin_button", "flib_pin_white", { "gui.flib-keep-open" }, main_gui.on_pin_button_clicked, true)
125  )
126  header.add(frame_action_button("close_button", "utility/close", { "gui.close-instruction" }, main_gui.hide, false))
127
128  gui_util.update_frame_action_button(header.nav_backward_button, "disabled")
129  gui_util.update_frame_action_button(header.nav_forward_button, "disabled")
130  gui_util.update_frame_action_button(header.show_unresearched_button, "selected")
131
132  local main_flow = window.add({ type = "flow", style = "inset_frame_container_horizontal_flow" })
133
134  --- @type MainGui
135  local self = {
136    context = context,
137    window = window,
138    header = header,
139    search_pane = search_pane.build(main_flow, context),
140    info_pane = info_pane.build(main_flow, context),
141    history = history.new(),
142    pinned = false,
143    update_when_opened = false,
144  }
145  setmetatable(self, mt)
146  storage.guis[player.index] = self
147
148  return self
149end
150
151--- @param player_index uint
152function main_gui.destroy(player_index)
153  local self = storage.guis[player_index]
154  if not self then
155    return
156  end
157  if self.window.valid then
158    self.window.destroy()
159  end
160  storage.guis[player_index] = nil
161end
162
163function main_gui:hide()
164  self.window.visible = false
165  if self.context.player.opened == self.window then
166    self.context.player.opened = nil
167  end
168end
169
170function main_gui:prev()
171  if not self.window.visible then
172    return
173  end
174  self.history:prev()
175  self:update_info()
176end
177
178function main_gui:next()
179  if not self.window.visible then
180    return
181  end
182  self.history:next()
183  self:update_info()
184end
185
186function main_gui:show()
187  if self.update_when_opened then
188    self.update_when_opened = false
189    self:update()
190  end
191  self.window.visible = true
192  self.window.bring_to_front()
193  if not self.pinned then
194    self.context.player.opened = self.window
195    self.window.force_auto_center()
196  end
197  if self.context.player.mod_settings["rb-auto-focus-search"].value then
198    self.search_pane:focus_search()
199  end
200end
201
202--- @return boolean
203function main_gui:visible()
204  local window = self.window
205  return window.valid and window.visible
206end
207
208function main_gui:toggle()
209  if self.window.visible then
210    main_gui.hide(self)
211  else
212    main_gui.show(self)
213  end
214end
215
216function main_gui:update_info()
217  gui_util.update_frame_action_button(
218    self.header.nav_backward_button,
219    self.history:at_front() and "disabled" or "default"
220  )
221  gui_util.update_frame_action_button(
222    self.header.nav_forward_button,
223    self.history:at_back() and "disabled" or "default"
224  )
225
226  local prototype = self.history:current()
227  if not prototype then
228    return
229  end
230  self.search_pane:select_result(prototype)
231  self.info_pane:show(prototype)
232end
233
234function main_gui:update()
235  log("Updating for " .. self.context.player.name)
236  self.search_pane:update()
237  self:update_info()
238end
239
240function main_gui:flag_for_update()
241  log("Flagging update for " .. self.context.player.name)
242  self.update_when_opened = true
243end
244
245--- @param prototype GenericPrototype
246function main_gui:show_page(prototype)
247  if self.history:push(prototype, self.context.grouping_mode) then
248    self:update_info()
249  end
250  self:show()
251end
252
253--- Get the player's GUI, if it exists.
254--- @param player_index uint
255--- @return MainGui?
256function main_gui.get(player_index)
257  if not storage.guis then
258    return
259  end
260  return storage.guis[player_index]
261end
262
263--- Get the player's GUI or create it if it does not exist
264--- @param player_index uint
265--- @return MainGui?
266function main_gui.get_or_create(player_index)
267  local self = main_gui.get(player_index)
268  if not self or not self.window.valid then
269    local player = game.get_player(player_index)
270    if not player then
271      return
272    end
273    if self then
274      player.print({ "message.rb-recreated-gui" })
275    end
276    self = main_gui.build(player)
277  end
278  return self
279end
280
281--- @param e EventData.on_gui_click
282function main_gui:on_pin_button_clicked(e)
283  self.pinned = e.element.toggled
284  if self.pinned then
285    self.header.close_button.tooltip = { "gui.close" }
286    self.search_pane.textfield.tooltip = { "gui.search" }
287    if self.context.player.opened == self.window then
288      self.context.player.opened = nil
289    end
290  else
291    self.context.player.opened = self.window
292    self.window.force_auto_center()
293    self.header.close_button.tooltip = { "gui.close-instruction" }
294    self.search_pane.textfield.tooltip = { "gui.flib-search-instruction" }
295  end
296end
297
298--- @param e EventData.on_gui_click
299function main_gui:on_show_hidden_button_clicked(e)
300  self.context.show_hidden = e.element.toggled
301  gui_util.update_frame_action_button(e.element, self.context.show_hidden and "selected" or "default")
302  self:update()
303end
304
305--- @param e EventData.on_gui_click
306function main_gui:on_show_unresearched_button_clicked(e)
307  self.context.show_unresearched = e.element.toggled
308  gui_util.update_frame_action_button(e.element, self.context.show_unresearched and "selected" or "default")
309  self:update()
310end
311
312--- @param e EventData.on_gui_click?
313function main_gui:on_titlebar_clicked(e)
314  if not e or e.button == defines.mouse_button_type.middle then
315    self.window.force_auto_center()
316  end
317end
318
319--- @param element LuaGuiElement
320function main_gui:on_pipette(element)
321  local tags = element.tags --[[@as ResultButtonTags]]
322  local id = tags.rb_id
323  if not id then
324    return
325  end
326  local context_id = tags.rb_context_id
327  if context_id then
328    local context = util.get_prototype(context_id)
329    if context then
330      --- @type LuaRecipePrototype?
331      local recipe = context.object_name == "LuaRecipePrototype" and context or grouped.recipe[util.get_path(context)]
332      if recipe then
333        local entity_prototype = util.get_prototype(id) --[[@as LuaEntityPrototype]]
334        util.create_blueprint(self.context.player, entity_prototype, recipe)
335        return
336      end
337    end
338  end
339  if id.type == "technology" then
340    return
341  end
342  local id_prototype = util.get_prototype(id) --[[@as PipetteID]]
343  self.context.player.pipette(id_prototype, nil, true)
344end
345
346--- @class ResultButtonTags
347--- @field rb_id DatabaseID?
348--- @field rb_context_id DatabaseID?
349
350--- @param e EventData.on_gui_click
351function main_gui:on_result_clicked(e)
352  local tags = e.element.tags --[[@as ResultButtonTags]]
353  local id = tags.rb_id
354  if not id then
355    return
356  end
357  if id.type == "technology" then
358    if not self.pinned then
359      self:hide()
360    end
361    self.context.player.open_technology_gui(id.name)
362    return
363  end
364  local prototype = prototypes[id.type][id.name]
365  assert(prototype, "Prototype was nil")
366  self.history:push(prototype, self.context.grouping_mode)
367  self:update_info()
368end
369
370function main_gui:on_window_closed()
371  if self.pinned then
372    return
373  end
374  self:hide()
375end
376
377-- Events
378
379--- @param force LuaForce
380local function update_force_guis(force)
381  for _, player in pairs(force.players) do
382    local player_gui = main_gui.get(player.index)
383    if player_gui then
384      if player_gui:visible() and player.connected then
385        player_gui:update()
386      else
387        player_gui:flag_for_update()
388      end
389    end
390  end
391end
392
393local function on_tick()
394  if not next(storage.update_force_guis or {}) then
395    return
396  end
397  for force_index in pairs(storage.update_force_guis) do
398    local force = game.forces[force_index]
399    if force then
400      update_force_guis(force)
401    end
402  end
403  storage.update_force_guis = {}
404end
405
406--- @type table<string, boolean>
407local setting_names = {
408  ["rb-grouping-mode"] = true,
409  ["rb-search-materials-only"] = true,
410}
411
412--- @param e EventData.on_runtime_mod_setting_changed
413local function on_runtime_mod_setting_changed(e)
414  if not storage.guis or not setting_names[e.setting] then
415    return
416  end
417
418  if storage.guis[e.player_index] then
419    main_gui.destroy(e.player_index)
420    local player = game.get_player(e.player_index)
421    --- @cast player -?
422    main_gui.build(player)
423  end
424end
425
426--- @param e EventData.on_research_finished|EventData.on_research_reversed
427local function on_research_updated(e)
428  if storage.update_force_guis then
429    storage.update_force_guis[e.research.force.index] = true
430  end
431end
432
433local allowed_base_types = {
434  entity = true,
435  equipment = true,
436  fluid = true,
437  item = true,
438  recipe = true,
439  tile = true,
440}
441
442--- @param e EventData.CustomInputEvent
443local function on_open_selected(e)
444  local selected_prototype = e.selected_prototype
445  if not selected_prototype or selected_prototype.base_type == "technology" then
446    return
447  end
448  local player_gui = main_gui.get_or_create(e.player_index)
449  if not player_gui then
450    return
451  end
452  if
453    selected_prototype.base_type == "entity"
454    and (selected_prototype.derived_type == "entity-ghost" or selected_prototype.derived_type == "tile-ghost")
455  then
456    local selected = player_gui.context.player.selected
457    if selected and (selected.type == "entity-ghost" or selected.type == "tile-ghost") then
458      selected_prototype.base_type = selected.type == "entity-ghost" and "entity" or "tile"
459      selected_prototype.derived_type = selected.ghost_type
460      selected_prototype.name = selected.ghost_name
461    end
462  end
463  if not allowed_base_types[selected_prototype.base_type] then
464    return
465  end
466  local prototype = prototypes[selected_prototype.base_type][selected_prototype.name]
467  player_gui:show_page(prototype)
468  if player_gui.history:push(prototype, player_gui.context.grouping_mode) then
469    player_gui:update_info()
470  end
471  player_gui:show()
472end
473
474--- @param e EventData.CustomInputEvent
475local function on_toggle(e)
476  local self = main_gui.get_or_create(e.player_index)
477  if self then
478    main_gui.toggle(self)
479  end
480end
481
482--- @param e EventData.on_lua_shortcut
483local function on_lua_shortcut(e)
484  if e.prototype_name ~= "rb-toggle" then
485    return
486  end
487  local self = main_gui.get_or_create(e.player_index)
488  if self then
489    main_gui.toggle(self)
490  end
491end
492
493--- @param e EventData.CustomInputEvent
494local function on_next(e)
495  local self = main_gui.get_or_create(e.player_index)
496  if self then
497    self:next()
498  end
499end
500
501--- @param e EventData.CustomInputEvent
502local function on_previous(e)
503  local self = main_gui.get_or_create(e.player_index)
504  if self then
505    self:prev()
506  end
507end
508
509--- @param e EventData.CustomInputEvent
510local function on_focus_search(e)
511  local self = storage.guis[e.player_index]
512  if self and self.window.valid and not self.pinned and self.window.visible then
513    self.search_pane:focus_search()
514  end
515end
516
517--- @param e EventData.CustomInputEvent
518local function on_pipette(e)
519  if not e.element then
520    return
521  end
522  local self = main_gui.get_or_create(e.player_index)
523  if self and self.context.player.is_cursor_empty() then
524    self:on_pipette(e.element)
525  end
526end
527
528--- @param e flib.on_player_dictionaries_ready
529local function on_player_dictionaries_ready(e)
530  local self = storage.guis[e.player_index]
531  if self and self.window.valid then
532    self.search_pane:update()
533  end
534end
535
536function main_gui.on_init()
537  --- @type table<uint, MainGui>
538  storage.guis = {}
539  --- @type table<uint, boolean>
540  storage.update_force_guis = {}
541end
542
543function main_gui.on_configuration_changed()
544  if not storage.guis then
545    return
546  end
547  for _, player in pairs(game.players) do
548    main_gui.destroy(player.index)
549  end
550end
551
552main_gui.events = {
553  [defines.events.on_gui_closed] = on_gui_closed,
554  [defines.events.on_lua_shortcut] = on_lua_shortcut,
555  [defines.events.on_research_finished] = on_research_updated,
556  [defines.events.on_research_reversed] = on_research_updated,
557  [defines.events.on_runtime_mod_setting_changed] = on_runtime_mod_setting_changed,
558  [defines.events.on_tick] = on_tick,
559  ["rb-linked-focus-search"] = on_focus_search,
560  ["rb-linked-pipette"] = on_pipette,
561  ["rb-next"] = on_next,
562  ["rb-open-selected"] = on_open_selected,
563  ["rb-previous"] = on_previous,
564  ["rb-toggle"] = on_toggle,
565  [flib_dictionary.on_player_dictionaries_ready] = on_player_dictionaries_ready,
566}
567
568flib_gui.add_handlers(main_gui, function(e, handler)
569  local pgui = main_gui.get_or_create(e.player_index)
570  if pgui then
571    handler(pgui, e)
572  end
573end, "main")
574
575info_pane.on_result_clicked = main_gui.on_result_clicked
576search_pane.on_result_clicked = main_gui.on_result_clicked
577
578-- commands.add_command("rb-test-info", "- Tests showing every possible Recipe Book info page", function(e)
579--   local gui = main_gui.get(e.player_index)
580--   if not gui then
581--     return
582--   end
583--   log("<< TESTING ALL INFO PAGES >>")
584--   local tested = {}
585--   local tested_count = 0
586--   local profiler = game.create_profiler()
587--   for _, entry in pairs(storage.database.entries) do
588--     local base_path = entry:get_path()
589--     if not tested[base_path] and not string.find(base_path, "technology/") then
590--       tested[base_path] = true
591--       tested_count = tested_count + 1
592--       gui.history:push(entry, gui.context.use_groups)
593--       gui:update_info()
594--     end
595--   end
596--   profiler.stop()
597--   log({ "", "Overall test ", profiler })
598--   log("Number of pages: " .. tested_count)
599--   profiler.divide(tested_count)
600--   log({ "", "Average test ", profiler })
601--   log("<< TEST COMPLETE >>")
602-- end)
603
604return main_gui