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..1569ae9f 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 @@ -100,6 +113,15 @@ function player_sync.serialize_player(player, player_record) local serialized_player = serialize.serialize_player(player, failed_deserialization, player_record.components) serialized_player.generation = player_record.generation + -- Leaving on the tick of a download the temporary bonus is still there and must not be uploaded. Removing it + -- from the character here would shrink the main inventory before the armor's bonus applies and spill items. + local raises = get_script_data().inventory_bonus_raises + local raise = raises and raises[player.name] + local character = serialized_player.character + if raise and character and raise.character == player.character then + character.character_inventory_slots_bonus = character.character_inventory_slots_bonus - raise.bonus + end + return serialized_player end @@ -134,10 +156,18 @@ 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 = + -- An earlier raise not removed yet is replaced, the character's inventory bonus is restored with the data + script_data.inventory_bonus_raises[player.name] = nil + 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, + } + end -- Restore player position and driving state restore_position(player, finished_record) @@ -396,6 +426,12 @@ player_sync.events[defines.events.on_player_joined_game] = function(event) script_data.active_uploads[player.name] = nil end +-- Raised for a restored armor after its equipment's inventory bonus applied, the temporary bonus is no longer needed +player_sync.events[defines.events.on_player_armor_inventory_changed] = function(event) + local player = assert(game.get_player(event.player_index)) + remove_inventory_bonus_raise(get_script_data(), player.name) +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..368766a6 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -10,6 +10,7 @@ local v2_storage_api = compat.version_ge("2.0.0") local v2_remote_controller = compat.version_ge("2.0.0") local v2_space_platform = compat.version_ge("2.0.0") local v2_exit_remote_view = compat.version_ge("2.0.56") +local v2_inventory_bonus_equipment = compat.version_ge("2.0.0") local recipe_notifications_api = compat.version_ge("2.0.67") local v2_0_quick_bar_api = compat.version_ge("2.0.0") local v2_1_quick_bar_api = compat.version_ge("2.1.0") @@ -73,6 +74,30 @@ 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 by the bonus of the armor's equipment which is +--- still to come, as far as a serialized main inventory needs it +--- @return integer raise Slots added to character_inventory_slots_bonus, removed again once the armor's bonus applies +local function raise_inventory_bonus(character, main) + local grid = character.grid --[[@as LuaEquipmentGrid]] + local equipment_bonus = 0 + for _, equipment in pairs(grid.equipment) do + equipment_bonus = equipment_bonus + equipment.inventory_bonus + end + local delayed = equipment_bonus - grid.inventory_bonus + local raise = math.max(0, math.min(last_slot_index(main) - #character.get_main_inventory(), delayed)) + 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 +215,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 +236,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 +1265,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 +1387,24 @@ 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 at the end of the tick, so once the armor is + -- restored the main inventory is raised by the part of that bonus still to come. Slots past it overflow and + -- are left over as without the raise. Only for armor with a grid, restoring it raises + -- on_player_armor_inventory_changed which removes the raise. 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 v2_inventory_bonus_equipment and stats and inventories.main and character and character.grid then + inventory_bonus_raise = raise_inventory_bonus(character, inventories.main) + end + end ) if stats then serialize.deserialize_character_grid(character, stats) @@ -1378,7 +1428,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