z_mirror/EvenDistributionLite
git clone https://git.raiguard.me/z_mirror/EvenDistributionLite
f71c37b
trunk
1--- @class DragState 2--- @field balance boolean 3--- @field entities LuaEntity[] 4--- @field item {name: string, quality: string} 5--- @field labels table<uint, LuaRenderObject> 6--- @field last_tick uint 7--- @field player LuaPlayer 8 9--- @class LastSelectedState 10--- @field cursor_count uint 11--- @field entity LuaEntity 12--- @field hand_location ItemStackLocation 13--- @field item ItemStackDefinition 14--- @field tick uint 15 16--- @type table<string, Color> 17local colors ={ 18red ={ r =1 } , 19white ={ r =1 , g =1 , b =1 } , 20yellow ={ r =1 , g =1 } , 21} 22 23--- @type table<string, defines.inventory[]> 24local entity_transfer_inventories ={ 25[ "agricultural-tower" ] ={ 26defines . inventory . crafter_input , 27defines . inventory . crafter_modules , 28defines . inventory . fuel , 29} , 30[ "ammo-turret" ] ={ defines . inventory . turret_ammo } , 31[ "artillery-turret" ] ={ defines . inventory . artillery_turret_ammo } , 32[ "artillery-wagon" ] ={ defines . inventory . artillery_wagon_ammo } , 33[ "assembling-machine" ] ={ 34defines . inventory . crafter_input , 35defines . inventory . crafter_modules , 36defines . inventory . fuel , 37} , 38[ "asteroid-collector" ] ={ defines . inventory . asteroid_collector_output } , 39[ "beacon" ] ={ defines . inventory . beacon_modules , defines . inventory . fuel } , 40[ "boiler" ] ={ defines . inventory . fuel } , 41[ "burner-generator" ] ={ defines . inventory . fuel } , 42[ "car" ] ={ defines . inventory . car_ammo , defines . inventory . car_trunk , defines . inventory . fuel } , 43[ "cargo-landing-pad" ] ={ defines . inventory . cargo_landing_pad_main } , 44[ "cargo-wagon" ] ={ defines . inventory . cargo_wagon } , 45[ "character" ] ={ 46defines . inventory . character_ammo , 47defines . inventory . character_armor , 48defines . inventory . character_guns , 49defines . inventory . character_main , 50defines . inventory . character_vehicle , 51} , 52[ "container" ] ={ defines . inventory . chest } , 53[ "furnace" ] ={ defines . inventory . crafter_input , defines . inventory . crafter_modules , defines . inventory . fuel } , 54[ "fusion-reactor" ] ={ defines . inventory . fuel } , 55[ "inserter" ] ={ defines . inventory . fuel } , 56[ "lab" ] ={ defines . inventory . lab_input , defines . inventory . lab_modules , defines . inventory . fuel } , 57[ "locomotive" ] ={ defines . inventory . fuel } , 58[ "logistic-container" ] ={ defines . inventory . chest } , 59[ "mining-drill" ] ={ defines . inventory . mining_drill_modules , defines . inventory . fuel } , 60[ "roboport" ] ={ defines . inventory . roboport_material , defines . inventory . roboport_robot , defines . inventory . fuel } , 61[ "rocket-silo" ] ={ 62defines . inventory . crafter_input , 63defines . inventory . crafter_modules , 64defines . inventory . fuel , 65defines . inventory . rocket_silo_rocket , 66} , 67[ "space-platform-hub" ] ={ defines . inventory . hub_main } , 68[ "spidertron" ] ={ defines . inventory . spider_ammo , defines . inventory . spider_trunk , defines . inventory . fuel } , 69} 70 71--- @type table<defines.controllers, defines.inventory[]> 72local player_transfer_inventories ={ 73[ defines . controllers . character ] ={ 74-- defines.inventory.character_ammo, 75defines . inventory . character_armor , 76defines . inventory . character_guns , 77defines . inventory . character_main , 78defines . inventory . character_vehicle , 79} , 80[ defines . controllers . cutscene ] ={} , 81[ defines . controllers . editor ] ={ 82-- defines.inventory.editor_ammo, 83defines . inventory . editor_armor , 84defines . inventory . editor_guns , 85defines . inventory . editor_main , 86} , 87[ defines . controllers . ghost ] ={} , 88[ defines . controllers . god ] ={ defines . inventory . god_main } , 89[ defines . controllers . remote ] ={} , 90[ defines . controllers . spectator ] ={} , 91} 92 93local complex_items ={ 94[ "item-with-entity-data" ] =true , 95[ "armor" ] =true , 96[ "spidertron-remote" ] =true , 97[ "blueprint" ] =true , 98[ "blueprint-book" ] =true , 99[ "upgrade-planner" ] =true , 100[ "deconstruction-planner" ] =true , 101} 102 103--- @param target LuaEntity|LuaPlayer 104--- @param inventory_defines defines.inventory[]? 105--- @return LuaInventory[] 106local function build_inventories ( target , inventory_defines ) 107if not inventory_defines then 108return {} 109end 110 111local out ={} 112for _ , inventory_type in pairs ( inventory_defines ) do 113-- Use table.insert here just in case get_inventory() returns nil. 114table . insert ( out , target . get_inventory ( inventory_type )) 115end 116return out 117end 118 119--- @alias TransferTarget LuaPlayer|LuaEntity|LuaInventory 120 121--- @param target TransferTarget 122--- @return LuaInventory[] 123local function get_transfer_inventories ( target ) 124if target . object_name == "LuaEntity" then 125return build_inventories ( target , entity_transfer_inventories [ target . type ]) 126elseif target . object_name == "LuaInventory" then 127return { target } 128elseif target . object_name == "LuaPlayer" then 129return build_inventories ( target , player_transfer_inventories [ target . controller_type ]) 130else 131error ( "Invalid transfer target type " .. target . object_name ) --- @diagnostic disable-line: undefined-field 132end 133end 134 135--- @param target TransferTarget 136--- @return fun(): LuaInventory? 137local function inventory_iterator ( target ) 138local inventories =get_transfer_inventories ( target ) 139local i =0 140return function () 141i =i + 1 142return inventories [ i ] 143end 144end 145 146--- @param from TransferTarget 147--- @param to TransferTarget 148--- @param spec ItemStackDefinition 149--- @return uint transferred 150local function transfer ( from , to , spec ) 151if spec . count < 0 then 152spec . count =math . abs ( spec . count ) 153local temp =from 154from =to 155to =temp 156end 157 158local from_inventories =inventory_iterator ( from ) 159local to_inventories =inventory_iterator ( to ) 160 161local from_cursor_stack , to_cursor_stack 162local from_cursor_stack_exhausted , to_cursor_stack_exhausted 163if from . object_name == "LuaPlayer" then 164from_cursor_stack =from . cursor_stack 165end 166if to . object_name == "LuaPlayer" then 167to_cursor_stack =to . cursor_stack 168end 169 170local transferred =0 171local id ={ name =spec . name , quality =spec . quality } 172 173local from_inventory =from_inventories () 174local to_inventory =to_inventories () 175while from_inventory and to_inventory and transferred < spec . count do 176local source_stack 177if from_cursor_stack and not from_cursor_stack_exhausted then 178source_stack =from_cursor_stack 179else 180source_stack =from_inventory . find_item_stack ( id ) 181end 182 183if 184source_stack 185and not from_cursor_stack_exhausted 186and ( 187not source_stack . valid_for_read or ( source_stack . name ~= spec . name or source_stack . quality . name ~= spec . quality ) 188) 189then 190from_cursor_stack_exhausted =true 191goto continue 192end 193 194if not source_stack then 195from_inventory =from_inventories () 196goto continue 197end 198 199local to_transfer =math . min ( source_stack . count , spec . count - transferred ) 200 201if to_cursor_stack and not to_cursor_stack_exhausted then 202if to_cursor_stack . valid_for_read then 203if 204to_cursor_stack . type == source_stack . type 205and to_cursor_stack . name == source_stack . name 206and to_cursor_stack . quality == source_stack . quality 207and to_cursor_stack . count < to_cursor_stack . prototype . stack_size 208then 209local count_before =to_cursor_stack . count 210to_cursor_stack . transfer_stack ( source_stack , to_transfer ) 211transferred =transferred + to_cursor_stack . count - count_before 212if not source_stack . valid_for_read then 213goto continue 214end 215end 216elseif to_cursor_stack . transfer_stack ( source_stack , to_transfer ) then 217transferred =transferred + to_cursor_stack . count 218to_cursor_stack_exhausted =true 219if source_stack == from_cursor_stack and not source_stack . valid_for_read then 220from_cursor_stack_exhausted =true 221end 222goto continue 223end 224end 225 226if not to_inventory . can_insert ( id ) then 227to_inventory =to_inventories () 228goto continue 229end 230 231if complex_items [ source_stack . type ] then 232local empty_slot =to_inventory . find_empty_stack ( id ) 233if not empty_slot then 234to_inventory =to_inventories () 235goto continue 236end 237assert ( empty_slot . transfer_stack ( source_stack ), "Transfer of full stack failed" ) 238transferred =transferred + empty_slot . count 239else 240--- @type ItemStackDefinition 241local this_spec ={ 242name =source_stack . name , 243quality =source_stack . quality . name , 244count =to_transfer , 245health =source_stack . health , 246durability =source_stack . type == "tool" and source_stack . durability or nil , 247ammo =source_stack . type == "ammo" and source_stack . ammo or nil , 248tags =source_stack . type == "item-with-tags" and source_stack . tags or nil , 249custom_description =source_stack . type == "item-with-tags" and source_stack . custom_description or nil , 250spoil_percent =source_stack . spoil_percent , 251} 252if this_spec . ammo and this_spec . ammo < 1 then 253-- Apparently the game will sometimes give us a LuaItemStack with an ammo of zero, despite the game attempting 254-- to enforce that this can never happen. Since I can't reproduce the issue, let's just set the ammo to 1 and 255-- call it a day. 256this_spec . ammo =1 257end 258local this_transferred =to_inventory . insert ( this_spec ) 259if this_transferred > 0 then 260source_stack . count =source_stack . count - this_transferred 261transferred =transferred + this_transferred 262else 263-- We weren't actually able to insert anything into this inventory, so move to the next one 264to_inventory =to_inventories () 265goto continue 266end 267end 268 269if source_stack == from_cursor_stack and not source_stack . valid_for_read then 270from_cursor_stack_exhausted =true 271end 272 273:: continue :: 274end 275 276return transferred 277end 278 279--- @param entity LuaEntity 280--- @param item ItemIDAndQualityIDPair 281--- @return uint 282local function get_entity_item_count ( entity , item ) 283local total =0 284local inventories =entity_transfer_inventories [ entity . type ] 285if not inventories then 286return 0 287end 288for _ , inventory_type in pairs ( inventories ) do 289local inventory =entity . get_inventory ( inventory_type ) 290if inventory then 291total =total + inventory . get_item_count ( item ) 292end 293end 294return total 295end 296 297--- @class EntityAndCount 298--- @field entity LuaEntity 299--- @field count integer 300 301--- @param a EntityAndCount 302--- @param b EntityAndCount 303local function sort_by_count ( a , b ) 304return a . count < b . count 305end 306 307--- @param entities LuaEntity[] 308--- @param item ItemIDAndQualityIDPair 309--- @param player_total uint 310--- @return EntityAndCount[] 311local function get_balanced_distribution ( entities , item , player_total ) 312local num_entities =# entities 313 314-- Determine total and individual entity contents 315--- @type uint[] 316local entity_counts ={} 317local total =player_total 318for i =1 , num_entities do 319local count =get_entity_item_count ( entities [ i ], item ) 320entity_counts [ i ] =count 321total =total + count 322end 323 324-- Get balanced distribution and calculate deltas for each entity 325local balanced =math . floor ( total / num_entities ) 326local remainder =total % num_entities 327--- @type EntityAndCount[] 328local out ={} 329for i =1 , num_entities do 330local entity_count =entity_counts [ i ] 331local target_count =balanced 332if remainder > 0 then 333remainder =remainder - 1 334target_count =target_count + 1 335end 336out [ i ] ={ entity =entities [ i ], count =target_count - entity_count } 337end 338table . sort ( out , sort_by_count ) 339return out 340end 341 342--- @param total uint 343--- @param entities LuaEntity[] 344--- @return EntityAndCount[] 345local function get_even_distribution ( total , entities ) 346local num_entities =# entities 347local base =math . floor ( total / num_entities ) 348local remainder =total % num_entities 349--- @type EntityAndCount[] 350local out ={} 351for i =1 , num_entities do 352local count =base 353if remainder > 0 then 354remainder =remainder - 1 355count =count + 1 356end 357out [ i ] ={ entity =entities [ i ], count =count } 358end 359table . sort ( out , sort_by_count ) 360return out 361end 362 363--- @param inventory LuaInventory 364--- @param cursor_stack LuaItemStack 365--- @param item ItemIDAndQualityIDPair 366--- @return uint 367local function get_item_count ( inventory , cursor_stack , item ) 368local count =inventory . get_item_count ( item ) 369if cursor_stack . valid_for_read and cursor_stack . name == item . name and cursor_stack . quality . name == item . quality then 370count =count + cursor_stack . count 371end 372return count 373end 374 375--- @param drag_state DragState 376local function validate_entities ( drag_state ) 377local entities ={} 378local i =0 379for _ , entity in pairs ( drag_state . entities ) do 380if entity . valid then 381i =i + 1 382entities [ i ] =entity 383end 384end 385drag_state . entities =entities 386end 387 388script . on_init ( function () 389--- @type table<uint, DragState> 390storage . drag ={} 391--- @type table<uint, LastSelectedState> 392storage . last_selected ={} 393end ) 394 395script . on_event ( defines . events . on_selected_entity_changed , function ( e ) 396local player =game . get_player ( e . player_index ) 397if not player or not player . valid then 398return 399end 400 401local selected =player . selected 402local cursor_stack =player . cursor_stack 403if 404not selected 405or selected . type == "loader" 406or selected . type == "loader-1x1" 407or not cursor_stack 408or not cursor_stack . valid_for_read 409then 410storage . last_selected [ e . player_index ] =nil 411return 412end 413local main_inventory =player . get_main_inventory () 414if not main_inventory then 415return 416end 417 418--- @type LastSelectedState 419storage . last_selected [ e . player_index ] ={ 420cursor_count =cursor_stack . count , 421entity =selected , 422hand_location =player . hand_location , 423item ={ 424name =cursor_stack . name , 425quality =cursor_stack . quality . name , 426count =get_item_count ( 427main_inventory , 428cursor_stack , 429{ name =cursor_stack . name , quality =cursor_stack . quality . name } 430), 431} , 432tick =game . tick , 433} 434end ) 435 436script . on_event ( defines . events . on_player_fast_transferred , function ( e ) 437if not e . from_player then 438return 439end 440 441local entity =e . entity 442if not entity . valid then 443return 444end 445 446local selected_state =storage . last_selected [ e . player_index ] 447if 448not selected_state 449or selected_state . tick ~= game . tick 450or not selected_state . entity . valid 451or selected_state . entity ~= entity 452then 453return 454end 455 456local player =game . get_player ( e . player_index ) 457if not player or not player . valid then 458return 459end 460 461local cursor_stack =player . cursor_stack 462if not cursor_stack then 463return 464end 465 466local main_inventory =player . get_main_inventory () 467if not main_inventory then 468return 469end 470 471-- Get the number of items that were inserted by the fast transfer 472local new_count =get_item_count ( main_inventory , cursor_stack , selected_state . item --[[@as ItemIDAndQualityIDPair]] ) 473local inserted =selected_state . item . count - new_count 474if inserted > 0 then 475-- Remove items from the destination and restore the player's inventory state 476local item ={ name =selected_state . item . name , quality =selected_state . item . quality , count =inserted } 477transfer ( entity , player , item ) 478elseif 479get_entity_item_count ( entity , { name =selected_state . item . name , quality =selected_state . item . quality } ) == 0 480then 481-- This item can't be inserted at all 482return 483end 484 485-- Create or retrieve drag state 486local drag_state =storage . drag [ e . player_index ] 487if not drag_state then 488--- @type DragState 489drag_state ={ 490balance =e . is_split ~= settings . get_player_settings ( player )[ "edl-swap-balance" ]. value , 491entities ={} , 492item ={ name =selected_state . item . name , quality =selected_state . item . quality } , 493last_tick =game . tick , 494labels ={} , 495player =player , 496} 497storage . drag [ e . player_index ] =drag_state 498end 499 500drag_state . last_tick =game . tick 501 502-- Remove game-generated flying text. 503player . clear_local_flying_texts () 504 505validate_entities ( drag_state ) 506 507local entities =drag_state . entities 508local labels =drag_state . labels 509local unit_number =entity . unit_number 510--- @cast unit_number -? 511 512if not labels [ unit_number ] then 513table . insert ( entities , entity ) 514end 515 516-- Update item counts 517local total =selected_state . item . count 518--- @cast total -? 519if drag_state . balance then 520for i =1 , # entities do 521total =total + get_entity_item_count ( entities [ i ], drag_state . item ) 522end 523end 524local distribution =get_even_distribution ( total , entities ) 525for _ , data in pairs ( distribution ) do 526local this_entity =data . entity 527local this_unit_number =this_entity . unit_number 528--- @cast this_unit_number -? 529local label =labels [ this_unit_number ] 530if not label or not label . valid then 531local color =colors . white 532if drag_state . balance then 533color =colors . yellow 534end 535label =rendering . draw_text ( { 536color =color , 537players ={ e . player_index } , 538surface =this_entity . surface , 539target =this_entity , 540text ="" , 541} ) 542labels [ this_unit_number ] =label 543end 544label . text =data . count 545end 546end ) 547 548--- @param drag_state DragState 549local function finish_drag ( drag_state ) 550if not drag_state . player . valid then 551return 552end 553 554-- Destroy labels 555for _ , label in pairs ( drag_state . labels ) do 556if label . valid then 557label . destroy () 558end 559end 560 561validate_entities ( drag_state ) 562 563local entities =drag_state . entities 564local item =drag_state . item 565local item_localised_name =prototypes . item [ item . name ]. localised_name 566if item . quality ~= "normal" then 567item_localised_name ={ "" , item_localised_name , " (" , prototypes . quality [ item . quality ]. localised_name , ")" } 568end 569 570local player =drag_state . player 571local cursor_stack =player . cursor_stack 572if not cursor_stack then 573return 574end 575local main_inventory =player . get_main_inventory () 576if not main_inventory then 577return 578end 579 580-- Calculate entity deltas 581local distribution 582local player_total =get_item_count ( main_inventory , cursor_stack , item ) 583if drag_state . balance then 584distribution =get_balanced_distribution ( entities , item , player_total ) 585else 586distribution =get_even_distribution ( player_total , entities ) 587end 588 589local max_total =player_total 590for _ , data in pairs ( distribution ) do 591if data . count < 0 then 592max_total =max_total + - data . count 593end 594end 595 596local required_stacks =math . ceil ( max_total / prototypes . item [ item . name ]. stack_size ) * 2 -- Just in case we have a bunch of non-joinable item stacks. 597local work_inventory =game . create_inventory ( required_stacks ) 598 599--- First, transfer everything from the player to the work inventory. 600assert ( 601transfer ( player , work_inventory , { name =item . name , quality =item . quality , count =player_total } ) == player_total , 602"Failed to transfer all items to work inventory." 603) 604 605for _ , data in pairs ( distribution ) do 606local entity =data . entity 607local to_insert =data . count 608 609local item_spec ={ name =item . name , count =to_insert , quality =item . quality } 610local transferred =transfer ( work_inventory , entity , item_spec ) 611 612-- Show flying text 613local color =colors . white 614if transferred == 0 then 615color =colors . red 616elseif transferred ~= math . abs ( to_insert ) then 617color =colors . yellow 618end 619--- @diagnostic disable-next-line missing-field 620player . create_local_flying_text ( { 621text ={ "" , to_insert > 0 and "-" or "+" , transferred , " [item=" , item_spec . name , "] " , item_localised_name } , 622position =entity . position , 623color =color , 624} ) 625end 626 627local remainder =work_inventory . get_item_count ( item ) 628if remainder > 0 then 629local transferred = 630transfer ( work_inventory , player , { name =item . name , quality =item . quality , count =remainder } ) 631if transferred < remainder then 632player . surface . spill_inventory ( { 633position =player . physical_position , 634inventory =work_inventory , 635force =player . force , 636allow_belts =false , 637} ) 638end 639end 640 641assert ( work_inventory . is_empty (), "Failed to empty work inventory." ) 642work_inventory . destroy () 643end 644 645script . on_event ( defines . events . on_player_cursor_stack_changed , function ( e ) 646local drag_state =storage . drag [ e . player_index ] 647if not drag_state then 648return 649end 650 651local cursor_stack =drag_state . player . cursor_stack 652local cursor_item =cursor_stack and cursor_stack . valid_for_read and cursor_stack . name 653if drag_state . item . name == cursor_item then 654return 655end 656 657storage . drag [ e . player_index ] =nil 658finish_drag ( drag_state ) 659end ) 660 661script . on_event ( defines . events . on_tick , function () 662for player_index , drag_state in pairs ( storage . drag ) do 663local clear_cursor =drag_state . player . mod_settings [ "edl-clear-cursor" ]. value --[[@as boolean]] 664if not clear_cursor then 665local ticks =drag_state . player . mod_settings [ "edl-ticks" ]. value --[[@as int]] 666if drag_state . last_tick + ticks <= game . tick then 667storage . drag [ player_index ] =nil 668finish_drag ( drag_state ) 669end 670end 671end 672end )