Keep main inventory slots from armor equipment when restoring a character

The inventory bonus of equipment in the armor's grid (toolbelts) is
applied by the engine on the tick after the armor is inserted. The main
inventory was restored in the same tick, so the items of every slot past
80 plus research plus the armor's own bonus did not fit and were put in
a corpse: legendary mech armor with legendary toolbelts left 2125 of
2330 slots in the corpse.

The armor is now restored first, the character's inventory bonus is
raised to fit the saved main inventory, and the raise is removed again
on the next tick, or before an upload if the player leaves first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILLISISandClaude Opus 5.5 committed 2026-10-18 22:34:05 +11:00
1 parent 43243eab86
commit b32f5416a1
3 files changed
+84 -6

No files matched your search

@@ -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 {}
+38 -1
View File
@@ -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
+45 -5
View File
@@ -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<string, LuaInventory>
--- @param inventories table<string, 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