diff --git a/plugins/player_sync/module/get_script_data.lua b/plugins/player_sync/module/get_script_data.lua index 52f207b7..7a1dbf8b 100644 --- a/plugins/player_sync/module/get_script_data.lua +++ b/plugins/player_sync/module/get_script_data.lua @@ -18,6 +18,7 @@ return function(no_early_return) player_sync.players_waiting_for_acquire = player_sync.players_waiting_for_acquire or {} player_sync.players_in_cutscene_to_sync = player_sync.players_in_cutscene_to_sync or {} player_sync.failed_deserialization = player_sync.failed_deserialization or {} + player_sync.inventory_bonus_raises = player_sync.inventory_bonus_raises or {} player_sync.active_downloads = player_sync.active_downloads or {} player_sync.finished_downloads = player_sync.finished_downloads or {} diff --git a/plugins/player_sync/module/player_sync.lua b/plugins/player_sync/module/player_sync.lua index 6285c8fe..6d9e7923 100644 --- a/plugins/player_sync/module/player_sync.lua +++ b/plugins/player_sync/module/player_sync.lua @@ -60,6 +60,19 @@ local function remove_player(player) script_data.players[player.name] = nil end +-- Remove the temporary inventory bonus a deserialized character got to fit its main inventory +local function remove_inventory_bonus_raise(script_data, player_name) + local raises = script_data.inventory_bonus_raises + local raise = raises and raises[player_name] + if raise then + raises[player_name] = nil + local character = raise.character + if character.valid then + character.character_inventory_slots_bonus = character.character_inventory_slots_bonus - raise.bonus + end + end +end + player_sync = {} -- This function is called by instance.js as /sc player_sync.download_inventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the controller @@ -73,6 +86,9 @@ end -- This function is called internally when a player leaves the game to -- serialize the player for upload. function player_sync.serialize_player(player, player_record) + -- Leaving on the tick of a download, the temporary bonus must not be uploaded + remove_inventory_bonus_raise(get_script_data(), player.name) + -- Editor mode is not supported. if player.controller_type == defines.controllers.editor then player.toggle_map_editor() @@ -134,10 +150,17 @@ function player_sync.deserialize_player(player, finished_record) -- Deserialize downloaded player data local serialized_player = compat.json_to_table(finished_record.data) assert(type(serialized_player) == "table", "wrong type for serialized_player") - local failed, spill = + local failed, spill, inventory_bonus_raise = serialize.deserialize_player(player, serialized_player, finished_record.components, local_controller) script_data.failed_deserialization[player.name] = failed player_record.local_controller = nil + if inventory_bonus_raise and inventory_bonus_raise > 0 then + script_data.inventory_bonus_raises[player.name] = { + character = player.character, + bonus = inventory_bonus_raise, + tick = game.tick, + } + end -- Restore player position and driving state restore_position(player, finished_record) @@ -396,6 +419,20 @@ player_sync.events[defines.events.on_player_joined_game] = function(event) script_data.active_uploads[player.name] = nil end +-- The inventory bonus of armor equipment applies on the tick after a download, the temporary bonus is no longer needed +player_sync.events[defines.events.on_tick] = function(event) + local script_data = get_script_data() + local raises = script_data.inventory_bonus_raises + if not raises or next(raises) == nil then + return + end + for player_name, raise in pairs(raises) do + if raise.tick < event.tick then + remove_inventory_bonus_raise(script_data, player_name) + end + end +end + player_sync.on_nth_tick = {} player_sync.on_nth_tick[33] = function(event) -- Periodically check players currently in the process of syncing diff --git a/plugins/player_sync/module/serialize.lua b/plugins/player_sync/module/serialize.lua index 7f9b3b2a..10bc518b 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -73,6 +73,23 @@ function serialize.deserialize_character(character, serialized) end end +--- Index of the last slot used by a serialized inventory +local function last_slot_index(serialized) + local last = 0 + for _, entry in ipairs(serialized.i) do + last = (entry.s or last + 1) + (entry.r or 0) + end + return last +end + +--- Raise the inventory bonus of a character with its armor restored to fit a serialized main inventory +--- @return integer raise Slots added to character_inventory_slots_bonus, to be removed again on the next tick +local function raise_inventory_bonus(character, main) + local raise = math.max(0, last_slot_index(main) - #character.get_main_inventory()) + character.character_inventory_slots_bonus = character.character_inventory_slots_bonus + raise + return raise +end + --- Restore the armor grid state, the grid exists after the armor inventory is restored function serialize.deserialize_character_grid(character, serialized) local grid = character.grid @@ -190,15 +207,18 @@ end --- @param destinations table --- @param inventories table? --- @param items { name: string, count: integer, quality: string? }[]? +--- @param after_armor fun()? Called once the armor is restored, before the other inventories --- @return table[] leftovers -function serialize.place_items(destinations, inventories, items) +function serialize.place_items(destinations, inventories, items, after_armor) for _, destination in pairs(destinations) do destination.clear() end - -- Every inventory is restored before inserting into them, as restoring a slot replaces what is in it + -- Every inventory is restored before inserting into them, as restoring a slot replaces what is in it. + -- The armor first, its inventory bonus changes the size of the main inventory. + inventories = inventories or {} local pending = {} - for name, serialized in pairs(inventories or {}) do + local function restore(name, serialized) local destination = destinations[name] local fitting, rest = split_inventory(serialized, destination and #destination or 0) if destination then @@ -208,6 +228,17 @@ function serialize.place_items(destinations, inventories, items) table.insert(pending, { from = destination, stack = stack }) end end + if inventories.armor then + restore("armor", inventories.armor) + end + if after_armor then + after_armor() + end + for name, serialized in pairs(inventories) do + if name ~= "armor" then + restore(name, serialized) + end + end for _, item in pairs(items or {}) do table.insert(pending, { stack = { n = item.name, c = item.count, q = item.quality } }) end @@ -1226,6 +1257,7 @@ end --- @param components SyncComponents --- @param local_controller LocalControllerState? Controller to restore when the controller is not synced --- @return FailedDeserializationPlayerData?, table[]? Serialized stacks which did not fit, see spill_items +--- @return integer? inventory_bonus_raise See raise_inventory_bonus function serialize.deserialize_player(player, serialized, components, local_controller) local failed_deserialization = {} local sync_controller = syncs(components, "controller") @@ -1347,14 +1379,22 @@ function serialize.deserialize_player(player, serialized, components, local_cont -- Deserialize items, a ghost got them while it had a character above local character = player.character + local inventory_bonus_raise if place and not to_ghost then local stats = character and items.character if stats then serialize.deserialize_character(character, stats) end local craft = character and queue + -- The bonus of the armor's equipment (toolbelts) only applies on the next tick, so once the armor is + -- restored the main inventory is raised to fit the data spill = serialize.place_items( - serialize.item_destinations(player), inventories, not craft and queue and queue.ingredients or nil + serialize.item_destinations(player), inventories, not craft and queue and queue.ingredients or nil, + function() + if stats and inventories.main then + inventory_bonus_raise = raise_inventory_bonus(character, inventories.main) + end + end ) if stats then serialize.deserialize_character_grid(character, stats) @@ -1378,7 +1418,7 @@ function serialize.deserialize_player(player, serialized, components, local_cont serialize.deserialize_crafting_notifications(player, serialized.recipe_notifications) end - return next(failed_deserialization) and failed_deserialization or nil, spill + return next(failed_deserialization) and failed_deserialization or nil, spill, inventory_bonus_raise end return serialize