z_mirror/RateCalculator

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

Caleb HeuerFix migrations494331b

trunk
16.9 KiB576 linesraw
1local flib_dictionary = require("__flib__.dictionary")
2local flib_format = require("__flib__.format")
3local flib_gui = require("__flib__.gui")
4local flib_math = require("__flib__.math")
5local flib_table = require("__flib__.table")
6
7local gui_util = require("scripts.gui-util")
8
9--- @alias DisplayCategory
10--- | "products"
11--- | "intermediates"
12--- | "ingredients"
13
14--- @alias GenericPrototype LuaEntityPrototype|LuaFluidPrototype|LuaItemPrototype
15
16--- @class RatesDisplayData: Rates
17--- @field category DisplayCategory
18--- @field path SpritePath
19--- @field sorting_rate double
20--- @field completed boolean
21--- @field is_watts boolean
22
23--- @alias CategoryDisplayData table<DisplayCategory, RatesDisplayData>
24--- @alias DisplayDataLookup table<string, RatesDisplayData>
25
26local colors = {
27  green = "150,255,150",
28  red = "255,150,150",
29  white = "255,255,255",
30}
31
32--- @param amount number
33--- @param prefer_si boolean
34--- @param positive_prefix boolean
35--- @return string
36local function format_number(amount, prefer_si, positive_prefix)
37  local formatted = ""
38  if prefer_si or math.abs(amount) >= 10000 then
39    formatted = flib_format.number(amount, true)
40  else
41    local precision = 0.001
42    if math.abs(amount) >= 1000 then
43      precision = 1
44    elseif math.abs(amount) >= 100 then
45      precision = 0.1
46    elseif math.abs(amount) >= 10 then
47      precision = 0.01
48    end
49    formatted = flib_format.number(flib_math.round(amount, precision))
50  end
51  if positive_prefix and amount > 0 then
52    formatted = "+" .. formatted
53  end
54  return formatted
55end
56
57--- @param counts MachineCounts
58--- @param include_numbers boolean
59--- @return string
60local function build_machine_icons(counts, include_numbers)
61  local output = ""
62  for name, count in pairs(counts) do
63    output = output .. "[entity=" .. name .. "] "
64    if include_numbers then
65      output = output .. count .. "  "
66    end
67  end
68  return output
69end
70
71--- @param rate Rate
72--- @param color string
73--- @param label LocalisedString
74--- @param suffix LocalisedString
75local function build_rate_tooltip(rate, color, label, suffix)
76  return {
77    "gui.rcalc-colored-caption",
78    {
79      "",
80      {
81        "gui.rcalc-tooltip-entry",
82        label,
83        { "", format_number(rate.rate, false, false), suffix },
84      },
85      {
86        "gui.rcalc-parenthesized-caption",
87        {
88          "gui.rcalc-machines-caption",
89          { "", format_number(rate.rate / rate.machines, false, false), suffix },
90          {
91            "gui.rcalc-caption-with-suffix",
92            format_number(rate.machines, false, false),
93            { "gui.rcalc-machines" },
94          },
95        },
96      },
97    },
98    color,
99  }
100end
101
102--- @param num number
103--- @return string
104local function get_net_color(num)
105  if num > 0 then
106    return colors.green
107  elseif num < 0 then
108    return colors.red
109  else
110    return colors.white
111  end
112end
113
114--- @param e EventData.on_gui_click
115local function on_completion_checkbox_checked(e)
116  local self = storage.gui[e.player_index]
117  if not self then
118    return
119  end
120  local set = self.sets[self.selected_set_index]
121  if set then
122    set.completed[e.element.name] = e.element.state or nil
123  end
124end
125
126--- @param e EventData.on_gui_click
127local function on_rates_flow_clicked(e)
128  if not e.alt then
129    return
130  end
131  if not remote.interfaces["RecipeBook"] or remote.call("RecipeBook", "version") ~= 5 then
132    return
133  end
134  --- @type string, string
135  local type, name
136  local icon_elem = e.element.icon
137  if icon_elem.type == "choose-elem-button" then
138    type = string.gsub(icon_elem.elem_type, "%-with%-quality", "")
139    name = icon_elem.elem_value.name
140  else
141    local sprite = e.element.icon.sprite
142    type, name = string.match(sprite, "(.*)/(.*)")
143  end
144  if not type or not name then
145    return
146  end
147  local prototype = prototypes[type][name]
148  if prototype then
149    remote.call("RecipeBook", "open_page", e.player_index, prototype)
150  end
151end
152
153--- @param e EventData.on_gui_hover
154local function on_rates_flow_hovered(e)
155  local self = storage.gui[e.player_index]
156  if not self or not self.elems.rcalc_window.valid then
157    return
158  end
159
160  local elem = e.element
161  local path = elem.name
162  local data = self.display_data_lookup[path]
163  if not data then
164    return
165  end
166
167  local category = data.category
168  local output = data.output
169  local input = data.input
170  local is_watts = data.is_watts
171  local suffix = is_watts and { "si-unit-symbol-watt" } or { "gui.rcalc-timescale-suffix-" .. self.selected_timescale }
172  --- @type Rate
173  local category_rate = category == "ingredients" and input or output
174
175  --- @type GenericPrototype
176  local prototype = prototypes[data.type][data.name]
177
178  local name = prototype.localised_name
179  if data.temperature then
180    name = {
181      "",
182      name,
183      " (",
184      format_number(data.temperature, false, false),
185      { "si-unit-degree-celsius" },
186      ")",
187    }
188  end
189  if data.quality and data.quality ~= "normal" then
190    name = {
191      "",
192      name,
193      " (",
194      prototypes.quality[data.quality].localised_name,
195      ")",
196    }
197  end
198
199  local machine_counts_caption = build_machine_icons(category_rate.machine_counts, true)
200
201  --- @type LocalisedString
202  local intermediate_breakdown_caption = { "" }
203  if category == "intermediates" then
204    machine_counts_caption = machine_counts_caption .. "→  " .. build_machine_icons(input.machine_counts, true)
205
206    local net_rate = output.rate - input.rate
207    rate_caption = {
208      "gui.rcalc-colored-caption",
209      {
210        "",
211        {
212          "gui.rcalc-tooltip-entry",
213          { "gui.rcalc-net-rate" },
214          { "", format_number(net_rate, false, true), suffix },
215        },
216        {
217          "gui.rcalc-parenthesized-caption",
218          {
219            "gui.rcalc-caption-with-suffix",
220            format_number(net_rate / (output.rate / output.machines), false, true),
221            { "gui.rcalc-machines" },
222          },
223        },
224      },
225      get_net_color(net_rate),
226    }
227
228    intermediate_breakdown_caption = {
229      "",
230      "\n\n",
231      build_rate_tooltip(output, colors.green, { "gui.rcalc-production" }, suffix),
232      "\n",
233      build_rate_tooltip(input, colors.red, { "gui.rcalc-consumption" }, suffix),
234    }
235  else
236    rate_caption = build_rate_tooltip(category_rate, colors.white, { "gui.rcalc-rate" }, suffix)
237  end
238
239  machine_counts_caption = machine_counts_caption .. "\n"
240
241  elem.tooltip = {
242    "",
243    { "gui.rcalc-tooltip-title", name },
244    machine_counts_caption,
245    rate_caption,
246    intermediate_breakdown_caption,
247    remote.interfaces["RecipeBook"]
248        and { "", "\n\n", { "gui.rcalc-open-in-recipe-book-instruction", { "mod-name.RecipeBook" } } }
249      or nil,
250  }
251end
252
253--- @param e EventData.on_gui_leave
254local function on_rates_flow_left(e)
255  e.element.tooltip = ""
256end
257
258flib_gui.add_handlers({
259  on_completion_checkbox_checked = on_completion_checkbox_checked,
260  on_rates_flow_clicked = on_rates_flow_clicked,
261  on_rates_flow_hovered = on_rates_flow_hovered,
262  on_rates_flow_left = on_rates_flow_left,
263})
264
265--- @param parent LuaGuiElement
266--- @param category DisplayCategory
267--- @param rates RatesDisplayData[]
268--- @param show_machines boolean
269--- @param show_checkboxes boolean
270--- @param show_breakdown boolean
271local function build_rates_table(parent, category, rates, show_machines, show_checkboxes, show_breakdown)
272  --- @type flib.GuiElemDef
273  local rates_table = { type = "table", style = "slot_table", column_count = 1 }
274
275  for _, data in pairs(rates) do
276    local output = data.output
277    local input = data.input
278    local category_rate = data.category == "ingredients" and input or output
279    local raw_rate = category_rate.rate
280
281    local machines = format_number(category_rate.machines, false, false)
282    local machines_caption = {
283      "gui.rcalc-machines-caption",
284      build_machine_icons(category_rate.machine_counts, false),
285      machines,
286    }
287
288    local rate_color = colors.white
289    if category == "intermediates" then
290      raw_rate = flib_math.round(output.rate - input.rate, 0.00001) -- Floating point sucks
291      rate_color = get_net_color(raw_rate)
292      local net_machines = raw_rate / (output.rate / output.machines)
293      machines_caption = {
294        "gui.rcalc-net-machines-caption",
295        machines_caption,
296        rate_color,
297        format_number(net_machines, false, true),
298      }
299    end
300
301    local rate_caption = format_number(raw_rate, data.is_watts, category == "intermediates")
302
303    local flow = {
304      type = "flow",
305      name = data.path,
306      style = "rcalc_rates_table_row_flow",
307      raise_hover_events = true,
308      game_controller_interaction = defines.game_controller_interaction and defines.game_controller_interaction.always,
309      handler = {
310        [defines.events.on_gui_click] = on_rates_flow_clicked,
311        [defines.events.on_gui_hover] = on_rates_flow_hovered,
312        [defines.events.on_gui_leave] = on_rates_flow_left,
313      },
314    }
315
316    if show_checkboxes then
317      flow[#flow + 1] = {
318        type = "checkbox",
319        name = data.path,
320        state = data.completed,
321        handler = {
322          [defines.events.on_gui_checked_state_changed] = on_completion_checkbox_checked,
323        },
324      }
325    end
326
327    local button_style = "rcalc_transparent_slot"
328    if data.path == "item/rcalc-heat-dummy/normal" then
329      button_style = "rcalc_transparent_slot_no_shadow"
330    end
331
332    if data.type == "item" then
333      flow[#flow + 1] = {
334        type = "choose-elem-button",
335        name = "icon",
336        style = button_style,
337        elem_type = "item-with-quality",
338        -- XXX: Setting the default value doesn't work nor does it support quality
339        elem_mods = { elem_value = { name = data.name, quality = data.quality } },
340        ignored_by_interaction = true,
341      }
342    else
343      flow[#flow + 1] = {
344        type = "sprite-button",
345        name = "icon",
346        style = button_style,
347        sprite = "fluid/" .. data.name,
348        number = data.temperature,
349        ignored_by_interaction = true,
350      }
351    end
352
353    if show_machines then
354      flow[#flow + 1] = {
355        type = "label",
356        style = "rcalc_machines_label",
357        caption = machines_caption,
358        ignored_by_interaction = true,
359      }
360      flow[#flow + 1] = { type = "empty-widget", style = "flib_horizontal_pusher", ignored_by_interaction = true }
361    end
362
363    if category == "intermediates" and show_breakdown then
364      flow[#flow + 1] = {
365        type = "label",
366        style = "rcalc_intermediate_breakdown_label",
367        caption = {
368          "",
369          { "gui.rcalc-colored-caption", format_number(output.rate, data.is_watts, false), colors.green },
370          " - ",
371          { "gui.rcalc-colored-caption", format_number(input.rate, data.is_watts, false), colors.red },
372        },
373        ignored_by_interaction = true,
374      }
375    end
376
377    flow[#flow + 1] = {
378      type = "label",
379      style = "rcalc_rate_label",
380      caption = {
381        "gui.rcalc-colored-caption",
382        { "", rate_caption, data.is_watts and { "si-unit-symbol-watt" } or "" },
383        rate_color,
384      },
385      ignored_by_interaction = true,
386    }
387    rates_table[#rates_table + 1] = flow
388  end
389
390  flib_gui.add(parent, {
391    type = "flow",
392    direction = "vertical",
393    { type = "label", style = "caption_label", caption = { "gui.rcalc-" .. category } },
394    rates_table,
395  })
396end
397
398local gui_rates = {}
399
400--- @param self GuiData
401--- @param set CalculationSet
402--- @return CategoryDisplayData
403function gui_rates.update_display_data(self, set)
404  local timescale_data = gui_util.timescale_data[self.selected_timescale]
405  local manual_multiplier = self.manual_multiplier
406  local multiplier = timescale_data.multiplier or 1
407  local divisor, type_filter, divide_stacks, inserter_stack_size = gui_util.get_divisor(self)
408  local dictionary = flib_dictionary.get(self.player.index, "search") or {}
409  local show_power_input = self.player.mod_settings["rcalc-show-power-consumption"].value --[[@as boolean]]
410  local show_pollution = self.player.mod_settings["rcalc-show-pollution"].value --[[@as boolean]]
411  local search_query = self.search_query
412
413  --- @param rate Rate
414  --- @param is_watts boolean
415  --- @return Rate
416  local function scale_rate(rate, is_watts)
417    local multiplier = is_watts and 1 or multiplier
418    local divisor = is_watts and 1 or divisor
419    return {
420      machine_counts = flib_table.mapped(rate.machine_counts, function(count)
421        return count * manual_multiplier
422      end),
423      machines = rate.machines * manual_multiplier,
424      rate = rate.rate / (divisor or 1) * multiplier * manual_multiplier,
425    }
426  end
427
428  --- @type table<DisplayCategory, RatesDisplayData[]>
429  local category_display_data = {
430    products = {},
431    intermediates = {},
432    ingredients = {},
433  }
434  --- @type DisplayDataLookup
435  local display_data_lookup = {}
436
437  for path, rates in pairs(set.rates) do
438    local is_watts = path == "item/rcalc-power-dummy/normal" or path == "item/rcalc-heat-dummy/normal"
439    local output = scale_rate(rates.output, is_watts)
440    local input = scale_rate(rates.input, is_watts)
441
442    if divide_stacks and rates.type == "item" and not is_watts then
443      local stack_size = prototypes.item[rates.name].stack_size
444      output.rate = output.rate / stack_size
445      input.rate = input.rate / stack_size
446    end
447
448    if inserter_stack_size and inserter_stack_size > 0 and rates.type == "item" and not is_watts then
449      local stack_size = math.min(prototypes.item[rates.name].stack_size, inserter_stack_size)
450      output.rate = output.rate / stack_size
451      input.rate = input.rate / stack_size
452    end
453
454    --- @type DisplayCategory
455    local category = "products"
456    local sorting_rate = output.rate
457    if output.rate > 0 and input.rate > 0 then
458      category = "intermediates"
459      sorting_rate = output.rate - input.rate
460    elseif input.rate > 0 then
461      category = "ingredients"
462      sorting_rate = input.rate
463    end
464
465    if type_filter and (type_filter ~= rates.type or is_watts or path == "item/rcalc-pollution-dummy/normal") then
466      goto continue
467    end
468    if path == "item/rcalc-power-dummy/normal" and not show_power_input then
469      if output.rate > 0 then
470        category = "products"
471      else
472        goto continue
473      end
474    end
475    if path == "item/rcalc-pollution-dummy/normal" and not show_pollution then
476      goto continue
477    end
478    local path_without_quality = string.match(path, "^(.*)/(.-)$")
479    local to_search = string.lower(dictionary[path_without_quality] or rates.name)
480    if not string.find(to_search, search_query, nil, true) then
481      goto continue
482    end
483
484    --- @type RatesDisplayData
485    local data = {
486      type = rates.type,
487      name = rates.name,
488      quality = rates.quality,
489      temperature = rates.temperature,
490      output = output,
491      input = input,
492      path = path,
493      category = category,
494      sorting_rate = sorting_rate,
495      completed = set.completed[path] or false,
496      is_watts = is_watts,
497    }
498    local category_data = category_display_data[category]
499    category_data[#category_data + 1] = data
500    display_data_lookup[path] = data
501
502    ::continue::
503  end
504
505  for _, rates in pairs(category_display_data) do
506    table.sort(rates, function(a, b)
507      return a.sorting_rate > b.sorting_rate
508    end)
509  end
510
511  self.display_data_lookup = display_data_lookup
512
513  return category_display_data
514end
515
516--- @param self GuiData
517--- @param category_display_data CategoryDisplayData
518function gui_rates.update_gui(self, category_display_data)
519  local show_checkboxes = self.player.mod_settings["rcalc-show-completion-checkboxes"].value --[[@as boolean]]
520  local show_intermediate_breakdowns = self.player.mod_settings["rcalc-show-intermediate-breakdowns"].value --[[@as boolean]]
521
522  local has_ingredients = #category_display_data.ingredients > 0
523  local has_intermediates = #category_display_data.intermediates > 0
524  local has_products = #category_display_data.products > 0
525
526  local rates_flow = self.elems.rates_flow
527  rates_flow.clear()
528
529  if has_ingredients then
530    build_rates_table(
531      rates_flow,
532      "ingredients",
533      category_display_data.ingredients,
534      not has_intermediates and not has_products,
535      show_checkboxes,
536      show_intermediate_breakdowns
537    )
538    if has_intermediates or has_products then
539      rates_flow.add({ type = "line", direction = "vertical" })
540    end
541  end
542  if has_intermediates or has_products then
543    rates_flow = rates_flow.add({ type = "flow", style = "rcalc_rates_table_vertical_flow", direction = "vertical" })
544  end
545
546  if has_products then
547    build_rates_table(
548      rates_flow,
549      "products",
550      category_display_data.products,
551      true,
552      show_checkboxes,
553      show_intermediate_breakdowns
554    )
555    if has_intermediates then
556      rates_flow.add({ type = "line", direction = "horizontal" })
557    end
558  end
559
560  if has_intermediates then
561    build_rates_table(
562      rates_flow,
563      "intermediates",
564      category_display_data.intermediates,
565      true,
566      show_checkboxes,
567      show_intermediate_breakdowns
568    )
569  end
570
571  if not has_ingredients and not has_intermediates and not has_products then
572    rates_flow.add({ type = "label", style = "rcalc_machines_label", caption = { "gui.rcalc-no-rates-to-display" } })
573  end
574end
575
576return gui_rates