z_mirror/RateCalculator

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

Caleb Heuerfix: Update to new temperature limits API6e542c8

trunk
19.7 KiB626 linesraw
1local util = require("util")
2
3local flib_bounding_box = require("__flib__.bounding-box")
4local flib_math = require("__flib__.math")
5local flib_table = require("__flib__.table")
6
7--- @alias RateCategory
8--- | "output"
9--- | "input"
10
11--- @class ResourceData
12--- @field occurrences uint
13--- @field products Product[]
14--- @field required_fluid Product?
15--- @field mining_time double
16
17--- @alias Timescale
18--- | "per-second",
19--- | "per-minute",
20--- | "per-hour",
21--- | "transport-belts",
22--- | "inserters",
23
24--- @class CalcUtil
25local calc_util = {}
26
27--- @param set CalculationSet
28--- @param error CalculationError
29function calc_util.add_error(set, error)
30  set.errors[error] = true
31end
32
33--- @param set CalculationSet
34--- @param category RateCategory
35--- @param type string
36--- @param name string
37--- @param quality string
38--- @param amount double
39--- @param invert boolean
40--- @param machine_name string?
41--- @param temperature double?
42function calc_util.add_rate(set, category, type, name, quality, amount, invert, machine_name, temperature)
43  local set_rates = set.rates
44  local path = type .. "/" .. name .. "/" .. quality .. (temperature or "")
45  local rates = set_rates[path]
46  if not rates then
47    if invert then
48      return -- Don't remove from rates that don't exist.
49    end
50    --- @type Rates
51    rates = {
52      type = type,
53      name = name,
54      quality = quality,
55      temperature = temperature,
56      output = { machines = 0, machine_counts = {}, rate = 0 },
57      input = { machines = 0, machine_counts = {}, rate = 0 },
58    }
59    set_rates[path] = rates
60  end
61  if invert then
62    amount = -amount
63  end
64  --- @type Rate
65  local rate = rates[category]
66  if machine_name then
67    local counts = rate.machine_counts
68    -- Don't remove a machine that doesn't exist
69    if not counts[machine_name] and invert then
70      goto no_rate
71    end
72    counts[machine_name] = (counts[machine_name] or 0) + (invert and -1 or 1)
73    if counts[machine_name] == 0 then
74      counts[machine_name] = nil
75    end
76  end
77  rate.rate = math.max(rate.rate + amount, 0)
78  rate.machines = rate.machines + (invert and -1 or 1)
79  -- Account for floating-point imprecision
80  if rate.rate < 0.00001 then
81    rate.rate = 0
82  end
83
84  ::no_rate::
85  if rates.input.machines == 0 and rates.output.machines == 0 then
86    set_rates[path] = nil
87  end
88end
89
90--- @param set CalculationSet
91--- @param entity LuaEntity
92--- @param invert boolean
93--- @param emissions_per_second double
94--- @return double
95function calc_util.process_burner(set, entity, invert, emissions_per_second)
96  local entity_prototype = entity.prototype
97  local burner_prototype = entity_prototype.burner_prototype --[[@as LuaBurnerPrototype]]
98  local burner = entity.burner --[[@as LuaBurner]]
99
100  local currently_burning = burner.currently_burning
101  if not currently_burning then
102    local item = burner.inventory.get_contents()[1]
103    if item then
104      currently_burning = { name = prototypes.item[item.name], quality = prototypes.quality[item.quality] }
105    end
106  end
107  if not currently_burning then
108    calc_util.add_error(set, "no-fuel")
109    return emissions_per_second
110  end
111
112  local currently_burning_prototype = currently_burning.name
113
114  local max_energy_usage = entity_prototype.get_max_energy_usage(entity.quality) * (entity.consumption_bonus + 1)
115  local burns_per_second = 1
116    / (currently_burning_prototype.fuel_value / (max_energy_usage / burner_prototype.effectivity) / 60)
117
118  calc_util.add_rate(
119    set,
120    "input",
121    "item",
122    currently_burning_prototype.name,
123    currently_burning.quality.name,
124    burns_per_second,
125    invert,
126    entity.name
127  )
128
129  local burnt_result = currently_burning_prototype.burnt_result
130  if burnt_result then
131    calc_util.add_rate(
132      set,
133      "output",
134      "item",
135      burnt_result.name,
136      currently_burning.quality.name,
137      burns_per_second,
138      invert,
139      entity.name
140    )
141  end
142
143  local emissions = (burner_prototype.emissions_per_joule[set.pollutant] or 0)
144    * 60
145    * max_energy_usage
146    * currently_burning_prototype.fuel_emissions_multiplier
147  return emissions_per_second + emissions
148end
149
150--- @param entity LuaEntity
151--- @param index uint
152--- @return LuaFluidPrototype?
153local function get_fluid(entity, index)
154  local filter = entity.has_fluid_segment(index) and entity.get_fluid_segment_filter(index)
155    or entity.get_fluid_filter(index)
156  if filter then
157    return filter
158  end
159
160  local fluid = entity.get_fluid(index)
161  if fluid then
162    return prototypes.fluid[fluid.name]
163  end
164end
165
166--- @param set CalculationSet
167--- @param entity LuaEntity
168function calc_util.process_beacon(set, entity)
169  if entity.status == defines.entity_status.no_power then
170    calc_util.add_error(set, "no-power")
171  end
172end
173
174--- @param set CalculationSet
175--- @param entity LuaEntity
176--- @param invert boolean
177function calc_util.process_boiler(set, entity, invert)
178  local entity_prototype = entity.prototype
179
180  -- heat-fluid-inside doesn't produce or consume any fluids, only the burner matters.
181  if entity_prototype.boiler_mode ~= "output-to-separate-pipe" then
182    return
183  end
184
185  local input_fluid = get_fluid(entity, 1)
186  if not input_fluid then
187    calc_util.add_error(set, "no-input-fluid")
188    return
189  end
190
191  local target_temperature = entity_prototype.target_temperature
192  --- @cast target_temperature -?
193
194  do
195    local minimum_temperature = entity_prototype.minimum_input_temperature or input_fluid.default_temperature
196    local energy_per_amount = (target_temperature - minimum_temperature) * input_fluid.heat_capacity
197    local fluid_usage = entity_prototype.get_max_energy_usage(entity.quality) / energy_per_amount * 60
198    calc_util.add_rate(set, "input", "fluid", input_fluid.name, "normal", fluid_usage, invert, entity.name)
199  end
200
201  local output_fluid = get_fluid(entity, 2)
202  if not output_fluid then
203    return
204  end
205
206  local minimum_temperature = entity_prototype.minimum_input_temperature or output_fluid.default_temperature
207  local energy_per_amount = (target_temperature - minimum_temperature) * output_fluid.heat_capacity
208  local fluid_usage = entity_prototype.get_max_energy_usage(entity.quality) / energy_per_amount * 60
209  calc_util.add_rate(set, "output", "fluid", output_fluid.name, "normal", fluid_usage, invert, entity.name)
210end
211
212--- @param set CalculationSet
213--- @param entity LuaEntity
214--- @param invert boolean
215--- @return double
216function calc_util.process_crafter(set, entity, invert, emissions_per_second)
217  local recipe, quality = entity.get_recipe()
218  if not recipe and entity.type == "furnace" then
219    local prev = entity.previous_recipe
220    if prev then
221      recipe = set.player.force.recipes[prev.name.name]
222      quality = prev.quality --[[@as LuaQualityPrototype]]
223    end
224  end
225  if not recipe then
226    calc_util.add_error(set, "no-recipe")
227    return emissions_per_second
228  end
229  --- @cast quality -?
230
231  local recipe_duration = recipe.energy / entity.crafting_speed
232
233  for _, ingredient in pairs(recipe.ingredients) do
234    local amount = ingredient.amount / recipe_duration
235    calc_util.add_rate(
236      set,
237      "input",
238      ingredient.type,
239      ingredient.name,
240      ingredient.type == "item" and quality.name or "normal",
241      amount,
242      invert,
243      entity.name
244    )
245  end
246
247  local productivity = 1
248    + math.min(entity.productivity_bonus + recipe.productivity_bonus, recipe.prototype.maximum_productivity)
249
250  for _, product in pairs(recipe.products) do
251    if product.type == "research-progress" then
252      goto continue
253    end
254
255    -- stylua: ignore start
256    local expected_amount = util.product_amount(product)
257    local productivity_base_complement = math.min(expected_amount, product.ignored_by_productivity or 0)
258    local productivity_base = expected_amount - productivity_base_complement
259    -- stylua: ignore end
260
261    local amount = (productivity_base_complement + productivity_base * productivity) / recipe_duration
262
263    calc_util.add_rate(
264      set,
265      "output",
266      product.type,
267      product.name,
268      product.type == "item" and quality.name or "normal",
269      amount,
270      invert,
271      entity.name,
272      product.temperature
273    )
274
275    ::continue::
276  end
277
278  return emissions_per_second * recipe.prototype.emissions_multiplier * (1 + entity.pollution_bonus)
279end
280
281--- @param set CalculationSet
282--- @param entity LuaEntity
283--- @param invert boolean
284--- @param emissions_per_second double
285--- @return double
286function calc_util.process_electric_energy_source(set, entity, invert, emissions_per_second)
287  local entity_prototype = entity.prototype
288
289  -- Electric energy interfaces can have their settings adjusted at runtime, so checking the energy source is pointless.
290  if entity.type == "electric-energy-interface" then
291    local production = entity.power_production * 60
292    if production > 0 then
293      calc_util.add_rate(set, "output", "item", "rcalc-power-dummy", "normal", production, invert, entity.name)
294    end
295    local usage = entity.power_usage * 60
296    if usage > 0 then
297      calc_util.add_rate(set, "input", "item", "rcalc-power-dummy", "normal", usage, invert, entity.name)
298    end
299    return emissions_per_second
300  end
301
302  local electric_energy_source_prototype = entity_prototype.electric_energy_source_prototype --[[@as LuaElectricEnergySourcePrototype]]
303
304  local added_emissions = 0
305  local max_energy_usage = entity_prototype.get_max_energy_usage(entity.quality) or 0
306  if max_energy_usage > 0 and max_energy_usage < flib_math.max_int53 then
307    local consumption_bonus = (entity.consumption_bonus + 1)
308    local drain = electric_energy_source_prototype.drain
309    local amount = max_energy_usage * consumption_bonus
310    if max_energy_usage ~= drain then
311      amount = amount + drain
312    end
313    calc_util.add_rate(set, "input", "item", "rcalc-power-dummy", "normal", amount * 60, invert, entity.name)
314    if entity.status == defines.entity_status.no_power then
315      calc_util.add_error(set, "no-power")
316    end
317    added_emissions = (electric_energy_source_prototype.emissions_per_joule[set.pollutant] or 0)
318      * (max_energy_usage * consumption_bonus)
319      * 60
320  end
321
322  local max_energy_production = entity_prototype.get_max_energy_production(entity.quality)
323  if max_energy_production > 0 and max_energy_production < flib_math.max_int53 then
324    if entity.type == "solar-panel" then
325      max_energy_production = max_energy_production
326        * entity.surface.solar_power_multiplier
327        * entity.surface.get_property("solar-power")
328        / prototypes.surface_property["solar-power"].default_value
329    end
330    calc_util.add_rate(
331      set,
332      "output",
333      "item",
334      "rcalc-power-dummy",
335      "normal",
336      max_energy_production * 60,
337      invert,
338      entity.name
339    )
340  end
341
342  return emissions_per_second + added_emissions
343end
344
345--- @param set CalculationSet
346--- @param entity LuaEntity
347--- @param invert boolean
348--- @param emissions_per_second double
349--- @return double
350function calc_util.process_fluid_energy_source(set, entity, invert, emissions_per_second)
351  --- @type LuaEntityPrototype
352  local entity_prototype = entity.prototype
353  local fluid_energy_source_prototype = entity_prototype.fluid_energy_source_prototype --[[@as LuaFluidEnergySourcePrototype]]
354
355  -- The fluid energy source fluidbox will always be the first one
356  local fluid_prototype
357  if entity.type == "boiler" then
358    fluid_prototype = get_fluid(entity, entity.fluids_count)
359  else
360    fluid_prototype = get_fluid(entity, 1)
361  end
362  if not fluid_prototype then
363    calc_util.add_error(set, "no-input-fluid")
364    return emissions_per_second
365  end
366  local max_energy_usage = entity_prototype.get_max_energy_usage(entity.quality) * (entity.consumption_bonus + 1)
367
368  local value
369  if fluid_energy_source_prototype.scale_fluid_usage then
370    if fluid_energy_source_prototype.burns_fluid and fluid_prototype.fuel_value > 0 then
371      value = max_energy_usage / (fluid_prototype.fuel_value / 60) / fluid_energy_source_prototype.effectivity
372    else
373      -- Now we need the actual fluid to get its temperature
374      local fluid = entity.get_fluid(entity.fluids_count)
375      if not fluid then
376        calc_util.add_error(set, "no-input-fluid")
377        return emissions_per_second
378      end
379      -- If the fluid is equal to its default temperature, then nothing will happen
380      local temperature_value = fluid.temperature - fluid_prototype.default_temperature
381      if temperature_value > 0 then
382        value = max_energy_usage
383          / (temperature_value * fluid_prototype.heat_capacity)
384          / fluid_energy_source_prototype.effectivity
385          * 60
386      end
387    end
388  else
389    value = fluid_energy_source_prototype.fluid_usage_per_tick / fluid_energy_source_prototype.effectivity * 60
390  end
391  if not value then
392    return emissions_per_second -- No error, but not rate either
393  end
394
395  calc_util.add_rate(set, "input", "fluid", fluid_prototype.name, "normal", value, invert, entity.name)
396
397  return (fluid_energy_source_prototype.emissions_per_joule[set.pollutant] or 0) * max_energy_usage * 60
398end
399
400--- @param set CalculationSet
401--- @param entity LuaEntity
402--- @param invert boolean
403function calc_util.process_generator(set, entity, invert)
404  local entity_prototype = entity.prototype
405  local fluid = get_fluid(entity, 1)
406  if not fluid then
407    calc_util.add_error(set, "no-input-fluid")
408    return
409  end
410  calc_util.add_rate(
411    set,
412    "input",
413    "fluid",
414    fluid.name,
415    "normal",
416    entity_prototype.get_fluid_usage_per_tick(entity.quality) * 60,
417    invert,
418    entity.name
419  )
420end
421
422--- @param set CalculationSet
423--- @param entity LuaEntity
424--- @param invert boolean
425function calc_util.process_heat_energy_source(set, entity, invert)
426  calc_util.add_rate(
427    set,
428    "input",
429    "item",
430    "rcalc-heat-dummy",
431    "normal",
432    entity.prototype.get_max_energy_usage(entity.quality) * (1 + entity.consumption_bonus) * 60,
433    invert,
434    entity.name
435  )
436end
437
438--- @param set CalculationSet
439--- @param entity LuaEntity
440--- @param invert boolean
441function calc_util.process_lab(set, entity, invert)
442  local research_data = set.research_data
443  if not research_data then
444    calc_util.add_error(set, "no-active-research")
445    return
446  end
447
448  local science_pack_drain = entity.prototype.science_pack_drain_rate_percent / 100
449  local research_multiplier = research_data.multiplier
450  local researching_speed = entity.prototype.get_researching_speed(entity.quality)
451  local speed_modifier = research_data.speed_modifier
452  -- TODO: Lab rates are wrong somehow. :(
453  -- XXX: Due to a bug with entity_speed_bonus, we must subtract the force's lab speed bonus and convert it to a
454  -- multiplicative relationship
455  local lab_multiplier = research_multiplier
456    * (entity.speed_bonus + 1)
457    * researching_speed
458    * science_pack_drain
459
460  local inputs = flib_table.inverted(entity.prototype.lab_inputs)
461  for _, ingredient in pairs(research_data.ingredients) do
462    if not inputs[ingredient.name] then
463      calc_util.add_error(set, "incompatible-science-packs")
464      return
465    end
466  end
467
468  for _, ingredient in ipairs(research_data.ingredients) do
469    -- TODO: Select quality
470    local durability = prototypes.item[ingredient.name].get_durability() or 1
471    local amount = ingredient.amount * lab_multiplier / durability
472    calc_util.add_rate(set, "input", "item", ingredient.name, "normal", amount, invert, entity.name)
473  end
474end
475
476--- @param set CalculationSet
477--- @param entity LuaEntity
478--- @param invert boolean
479function calc_util.process_mining_drill(set, entity, invert)
480  local entity_prototype = entity.prototype
481  local entity_productivity_bonus = entity.productivity_bonus
482  local entity_speed_bonus = entity.speed_bonus
483
484  -- Look for resource entities under the drill
485  local radius = entity_prototype.mining_drill_radius + 0.01
486  local box = flib_bounding_box.from_dimensions(entity.position, radius * 2, radius * 2)
487  local resource_entities = entity.surface.find_entities_filtered({ area = box })
488  local resource_entities_len = #resource_entities
489  if resource_entities_len == 0 then
490    calc_util.add_error(set, "no-mineable-resources")
491    return
492  end
493
494  --- @type table<string, ResourceData>
495  local resources = {}
496  local num_resource_entities = 0
497  local has_fluidbox = next(entity_prototype.fluidbox_prototypes) and true or false
498  local resource_categories = entity_prototype.resource_categories or {}
499  for i = 1, resource_entities_len do
500    local resource = resource_entities[i]
501    local resource_name = resource.name
502
503    -- If this resource has already been processed
504    local resource_data = resources[resource_name]
505    if resource_data then
506      resource_data.occurrences = resource_data.occurrences + 1
507      num_resource_entities = num_resource_entities + 1
508      goto continue
509    end
510
511    local resource_prototype = resource.prototype
512    if not resource_categories[resource_prototype.resource_category] then
513      goto continue
514    end
515    num_resource_entities = num_resource_entities + 1
516    local mineable_properties = resource_prototype.mineable_properties
517    local required_fluid = mineable_properties.required_fluid
518    if required_fluid and not has_fluidbox then
519      goto continue
520    end
521
522    resource_data = {
523      occurrences = 1,
524      products = mineable_properties.products,
525      mining_time = mineable_properties.mining_time,
526    }
527
528    if resource_prototype.infinite_resource then
529      resource_data.mining_time = resource_data.mining_time
530        / (resource.amount / resource_prototype.normal_resource_amount)
531    end
532
533    if required_fluid then
534      resource_data.required_fluid = {
535        type = "fluid",
536        name = required_fluid,
537        amount = mineable_properties.fluid_amount / 10, -- Ten mining operations per fluid consumed
538        probability = 1,
539      }
540    end
541
542    resources[resource_name] = resource_data
543
544    ::continue::
545  end
546
547  if num_resource_entities == 0 then
548    calc_util.add_error(set, "no-mineable-resources")
549    return
550  end
551
552  -- Process resource entities
553
554  local adjusted_mining_speed = entity_prototype.mining_speed
555    * (entity_speed_bonus + 1)
556    * (entity_productivity_bonus + 1)
557
558  for _, resource_data in pairs(resources) do
559    local resource_multiplier = (adjusted_mining_speed / resource_data.mining_time)
560      * (resource_data.occurrences / num_resource_entities)
561
562    -- Add required fluid to inputs
563    local required_fluid = resource_data.required_fluid
564    if required_fluid then
565      -- Productivity does not apply to ingredients
566      local fluid_per_second = required_fluid.amount * resource_multiplier / (entity_productivity_bonus + 1)
567
568      -- Add to inputs table
569      local fluid_name = required_fluid.name
570      calc_util.add_rate(set, "input", "fluid", fluid_name, "normal", fluid_per_second, invert, entity.name)
571    end
572
573    -- Iterate each product
574    for _, product in pairs(resource_data.products or {}) do
575      -- Get rate per second for this product on this drill, accounting for amount range and probability
576      local adjusted_product_per_second = util.product_amount(product) * resource_multiplier
577
578      -- Add to outputs table
579      calc_util.add_rate(
580        set,
581        "output",
582        product.type,
583        product.name,
584        "normal",
585        adjusted_product_per_second,
586        invert,
587        entity.name,
588        product.temperature
589      )
590    end
591  end
592end
593
594--- @param set CalculationSet
595--- @param entity LuaEntity
596--- @param invert boolean
597function calc_util.process_offshore_pump(set, entity, invert)
598  local fluid = entity.get_fluid(1)
599  if not fluid then
600    return
601  end
602
603  local pumping_speed = entity.prototype.get_pumping_speed(entity.quality)
604  calc_util.add_rate(set, "output", "fluid", fluid.name, "normal", pumping_speed * 60, invert, entity.name)
605end
606
607--- @param set CalculationSet
608--- @param entity LuaEntity
609--- @param invert boolean
610function calc_util.process_reactor(set, entity, invert)
611  calc_util.add_rate(
612    set,
613    "output",
614    "item",
615    "rcalc-heat-dummy",
616    "normal",
617    entity.prototype.get_max_energy_usage(entity.quality)
618      * (1 + entity.neighbour_bonus)
619      * (1 + entity.consumption_bonus)
620      * 60,
621    invert,
622    entity.name
623  )
624end
625
626return calc_util