z_mirror/RecipeBook
git clone https://git.raiguard.me/z_mirror/RecipeBook
6169e8f
trunk
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 ) 18return { 19type ="sprite-button" , 20name =name , 21style ="frame_action_button" , 22sprite =sprite , 23tooltip =tooltip , 24mouse_button_filter ={ "left" } , 25tags =flib_gui . format_handlers ( { [ defines . events . on_gui_click ] =handler } ), 26auto_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 ) 53main_gui . destroy ( player . index ) 54 55--- @type MainGuiContext 56local context ={ 57grouping_mode =player . mod_settings [ "rb-grouping-mode" ]. value --[[@as string]] , 58search_materials_only =player . mod_settings [ "rb-search-materials-only" ]. value --[[@as boolean]] , 59show_hidden =false , 60show_unresearched =true , 61player =player , 62} 63 64local window =player . gui . screen . add ( { 65type ="frame" , 66name ="rb_main_window" , 67direction ="vertical" , 68visible =false , 69tags =flib_gui . format_handlers ( { [ defines . events . on_gui_closed ] =main_gui . on_window_closed } ), 70} ) 71window . auto_center =true 72 73local header =window . add ( { 74type ="flow" , 75style ="flib_titlebar_flow" , 76tags =flib_gui . format_handlers ( { [ defines . events . on_gui_click ] =main_gui . on_titlebar_clicked } ), 77} ) 78header . drag_target =window 79header . add ( { 80type ="label" , 81style ="flib_frame_title" , 82caption ={ "gui.rb-title" } , 83ignored_by_interaction =true , 84} ) 85header . add ( { type ="empty-widget" , style ="flib_titlebar_drag_handle" , ignored_by_interaction =true } ) 86header . add ( 87frame_action_button ( 88"show_unresearched_button" , 89"rb_show_unresearched" , 90{ "gui.rb-show-unresearched-instruction" } , 91main_gui . on_show_unresearched_button_clicked , 92true 93) 94) 95header . add ( 96frame_action_button ( 97"show_hidden_button" , 98"rb_show_hidden" , 99{ "gui.rb-show-hidden-instruction" } , 100main_gui . on_show_hidden_button_clicked , 101true 102) 103) 104header . show_hidden_button . visible =util . show_hidden_enabled 105header . add ( 106frame_action_button ( 107"nav_backward_button" , 108"flib_nav_backward_white" , 109{ "gui.rb-nav-backward-instruction" } , 110main_gui . prev , 111false 112) 113) 114header . add ( 115frame_action_button ( 116"nav_forward_button" , 117"flib_nav_forward_white" , 118{ "gui.rb-nav-forward-instruction" } , 119main_gui . next , 120false 121) 122) 123header . add ( 124frame_action_button ( "pin_button" , "flib_pin_white" , { "gui.flib-keep-open" } , main_gui . on_pin_button_clicked , true ) 125) 126header . add ( frame_action_button ( "close_button" , "utility/close" , { "gui.close-instruction" } , main_gui . hide , false )) 127 128gui_util . update_frame_action_button ( header . nav_backward_button , "disabled" ) 129gui_util . update_frame_action_button ( header . nav_forward_button , "disabled" ) 130gui_util . update_frame_action_button ( header . show_unresearched_button , "selected" ) 131 132local main_flow =window . add ( { type ="flow" , style ="inset_frame_container_horizontal_flow" } ) 133 134--- @type MainGui 135local self ={ 136context =context , 137window =window , 138header =header , 139search_pane =search_pane . build ( main_flow , context ), 140info_pane =info_pane . build ( main_flow , context ), 141history =history . new (), 142pinned =false , 143update_when_opened =false , 144} 145setmetatable ( self , mt ) 146storage . guis [ player . index ] =self 147 148return self 149end 150 151--- @param player_index uint 152function main_gui . destroy ( player_index ) 153local self =storage . guis [ player_index ] 154if not self then 155return 156end 157if self . window . valid then 158self . window . destroy () 159end 160storage . guis [ player_index ] =nil 161end 162 163function main_gui : hide () 164self . window . visible =false 165if self . context . player . opened == self . window then 166self . context . player . opened =nil 167end 168end 169 170function main_gui : prev () 171if not self . window . visible then 172return 173end 174self . history : prev () 175self : update_info () 176end 177 178function main_gui : next () 179if not self . window . visible then 180return 181end 182self . history : next () 183self : update_info () 184end 185 186function main_gui : show () 187if self . update_when_opened then 188self . update_when_opened =false 189self : update () 190end 191self . window . visible =true 192self . window . bring_to_front () 193if not self . pinned then 194self . context . player . opened =self . window 195self . window . force_auto_center () 196end 197if self . context . player . mod_settings [ "rb-auto-focus-search" ]. value then 198self . search_pane : focus_search () 199end 200end 201 202--- @return boolean 203function main_gui : visible () 204local window =self . window 205return window . valid and window . visible 206end 207 208function main_gui : toggle () 209if self . window . visible then 210main_gui . hide ( self ) 211else 212main_gui . show ( self ) 213end 214end 215 216function main_gui : update_info () 217gui_util . update_frame_action_button ( 218self . header . nav_backward_button , 219self . history : at_front () and "disabled" or "default" 220) 221gui_util . update_frame_action_button ( 222self . header . nav_forward_button , 223self . history : at_back () and "disabled" or "default" 224) 225 226local prototype =self . history : current () 227if not prototype then 228return 229end 230self . search_pane : select_result ( prototype ) 231self . info_pane : show ( prototype ) 232end 233 234function main_gui : update () 235log ( "Updating for " .. self . context . player . name ) 236self . search_pane : update () 237self : update_info () 238end 239 240function main_gui : flag_for_update () 241log ( "Flagging update for " .. self . context . player . name ) 242self . update_when_opened =true 243end 244 245--- @param prototype GenericPrototype 246function main_gui : show_page ( prototype ) 247if self . history : push ( prototype , self . context . grouping_mode ) then 248self : update_info () 249end 250self : 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 ) 257if not storage . guis then 258return 259end 260return 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 ) 267local self =main_gui . get ( player_index ) 268if not self or not self . window . valid then 269local player =game . get_player ( player_index ) 270if not player then 271return 272end 273if self then 274player . ( { "message.rb-recreated-gui" } ) 275end 276self =main_gui . build ( player ) 277end 278return self 279end 280 281--- @param e EventData.on_gui_click 282function main_gui : on_pin_button_clicked ( e ) 283self . pinned =e . element . toggled 284if self . pinned then 285self . header . close_button . tooltip ={ "gui.close" } 286self . search_pane . textfield . tooltip ={ "gui.search" } 287if self . context . player . opened == self . window then 288self . context . player . opened =nil 289end 290else 291self . context . player . opened =self . window 292self . window . force_auto_center () 293self . header . close_button . tooltip ={ "gui.close-instruction" } 294self . search_pane . textfield . tooltip ={ "gui.flib-search-instruction" } 295end 296end 297 298--- @param e EventData.on_gui_click 299function main_gui : on_show_hidden_button_clicked ( e ) 300self . context . show_hidden =e . element . toggled 301gui_util . update_frame_action_button ( e . element , self . context . show_hidden and "selected" or "default" ) 302self : update () 303end 304 305--- @param e EventData.on_gui_click 306function main_gui : on_show_unresearched_button_clicked ( e ) 307self . context . show_unresearched =e . element . toggled 308gui_util . update_frame_action_button ( e . element , self . context . show_unresearched and "selected" or "default" ) 309self : update () 310end 311 312--- @param e EventData.on_gui_click? 313function main_gui : on_titlebar_clicked ( e ) 314if not e or e . button == defines . mouse_button_type . middle then 315self . window . force_auto_center () 316end 317end 318 319--- @param element LuaGuiElement 320function main_gui : on_pipette ( element ) 321local tags =element . tags --[[@as ResultButtonTags]] 322local id =tags . rb_id 323if not id then 324return 325end 326local context_id =tags . rb_context_id 327if context_id then 328local context =util . get_prototype ( context_id ) 329if context then 330--- @type LuaRecipePrototype? 331local recipe =context . object_name == "LuaRecipePrototype" and context or grouped . recipe [ util . get_path ( context )] 332if recipe then 333local entity_prototype =util . get_prototype ( id ) --[[@as LuaEntityPrototype]] 334util . create_blueprint ( self . context . player , entity_prototype , recipe ) 335return 336end 337end 338end 339if id . type == "technology" then 340return 341end 342local id_prototype =util . get_prototype ( id ) --[[@as PipetteID]] 343self . 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 ) 352local tags =e . element . tags --[[@as ResultButtonTags]] 353local id =tags . rb_id 354if not id then 355return 356end 357if id . type == "technology" then 358if not self . pinned then 359self : hide () 360end 361self . context . player . open_technology_gui ( id . name ) 362return 363end 364local prototype =prototypes [ id . type ][ id . name ] 365assert ( prototype , "Prototype was nil" ) 366self . history : push ( prototype , self . context . grouping_mode ) 367self : update_info () 368end 369 370function main_gui : on_window_closed () 371if self . pinned then 372return 373end 374self : hide () 375end 376 377-- Events 378 379--- @param force LuaForce 380local function update_force_guis ( force ) 381for _ , player in pairs ( force . players ) do 382local player_gui =main_gui . get ( player . index ) 383if player_gui then 384if player_gui : visible () and player . connected then 385player_gui : update () 386else 387player_gui : flag_for_update () 388end 389end 390end 391end 392 393local function on_tick () 394if not next ( storage . update_force_guis or {} ) then 395return 396end 397for force_index in pairs ( storage . update_force_guis ) do 398local force =game . forces [ force_index ] 399if force then 400update_force_guis ( force ) 401end 402end 403storage . 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 ) 414if not storage . guis or not setting_names [ e . setting ] then 415return 416end 417 418if storage . guis [ e . player_index ] then 419main_gui . destroy ( e . player_index ) 420local player =game . get_player ( e . player_index ) 421--- @cast player -? 422main_gui . build ( player ) 423end 424end 425 426--- @param e EventData.on_research_finished|EventData.on_research_reversed 427local function on_research_updated ( e ) 428if storage . update_force_guis then 429storage . update_force_guis [ e . research . force . index ] =true 430end 431end 432 433local allowed_base_types ={ 434entity =true , 435equipment =true , 436fluid =true , 437item =true , 438recipe =true , 439tile =true , 440} 441 442--- @param e EventData.CustomInputEvent 443local function on_open_selected ( e ) 444local selected_prototype =e . selected_prototype 445if not selected_prototype or selected_prototype . base_type == "technology" then 446return 447end 448local player_gui =main_gui . get_or_create ( e . player_index ) 449if not player_gui then 450return 451end 452if 453selected_prototype . base_type == "entity" 454and ( selected_prototype . derived_type == "entity-ghost" or selected_prototype . derived_type == "tile-ghost" ) 455then 456local selected =player_gui . context . player . selected 457if selected and ( selected . type == "entity-ghost" or selected . type == "tile-ghost" ) then 458selected_prototype . base_type =selected . type == "entity-ghost" and "entity" or "tile" 459selected_prototype . derived_type =selected . ghost_type 460selected_prototype . name =selected . ghost_name 461end 462end 463if not allowed_base_types [ selected_prototype . base_type ] then 464return 465end 466local prototype =prototypes [ selected_prototype . base_type ][ selected_prototype . name ] 467player_gui : show_page ( prototype ) 468if player_gui . history : push ( prototype , player_gui . context . grouping_mode ) then 469player_gui : update_info () 470end 471player_gui : show () 472end 473 474--- @param e EventData.CustomInputEvent 475local function on_toggle ( e ) 476local self =main_gui . get_or_create ( e . player_index ) 477if self then 478main_gui . toggle ( self ) 479end 480end 481 482--- @param e EventData.on_lua_shortcut 483local function on_lua_shortcut ( e ) 484if e . prototype_name ~= "rb-toggle" then 485return 486end 487local self =main_gui . get_or_create ( e . player_index ) 488if self then 489main_gui . toggle ( self ) 490end 491end 492 493--- @param e EventData.CustomInputEvent 494local function on_next ( e ) 495local self =main_gui . get_or_create ( e . player_index ) 496if self then 497self : next () 498end 499end 500 501--- @param e EventData.CustomInputEvent 502local function on_previous ( e ) 503local self =main_gui . get_or_create ( e . player_index ) 504if self then 505self : prev () 506end 507end 508 509--- @param e EventData.CustomInputEvent 510local function on_focus_search ( e ) 511local self =storage . guis [ e . player_index ] 512if self and self . window . valid and not self . pinned and self . window . visible then 513self . search_pane : focus_search () 514end 515end 516 517--- @param e EventData.CustomInputEvent 518local function on_pipette ( e ) 519if not e . element then 520return 521end 522local self =main_gui . get_or_create ( e . player_index ) 523if self and self . context . player . is_cursor_empty () then 524self : on_pipette ( e . element ) 525end 526end 527 528--- @param e flib.on_player_dictionaries_ready 529local function on_player_dictionaries_ready ( e ) 530local self =storage . guis [ e . player_index ] 531if self and self . window . valid then 532self . search_pane : update () 533end 534end 535 536function main_gui . on_init () 537--- @type table<uint, MainGui> 538storage . guis ={} 539--- @type table<uint, boolean> 540storage . update_force_guis ={} 541end 542 543function main_gui . on_configuration_changed () 544if not storage . guis then 545return 546end 547for _ , player in pairs ( game . players ) do 548main_gui . destroy ( player . index ) 549end 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 ) 569local pgui =main_gui . get_or_create ( e . player_index ) 570if pgui then 571handler ( pgui , e ) 572end 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