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