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