mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
Move items to whatever controller the player ends up with
Syncing the controller required syncing the inventories, and with the controller off a god or ghost kept its local items while the stored character was left alone. Cooldude pointed out there is no reason the items can't follow the player to a different controller. The controller and inventories components are now independent. The god inventory and the hidden ghost inventory belong to the inventories component. On download the scenario first settles the controller, synced or local, and then places the stored items into whatever that controller holds. An inventory goes slot for slot into the inventory of the same name when it fits, otherwise its items are inserted into the main, guns, ammo and armor inventories, and anything left goes into a corpse below the player. A spectator holds nothing, so everything goes into the corpse. A player without a character gets the crafting queue ingredients as items. This removes the early respawn of a ghost with a stored character and the upload of the respawned character, a ghost now gets and uploads its hidden inventory. Download spectates every synced player when the inventories are synced, as a god or ghost now holds synced items too. The controller keeps the stored logistic requests when the upload came from a player with nowhere to hold them. Slot filters are dropped when the destination does not support them, such as a script inventory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3d67ed7d9
commit
aa06b9afe3
5 files changed
+256
-100
No files matched your search
@@ -27,16 +27,6 @@ function serialize.serialize_inventories(source, inventories)
|
||||
return serialized
|
||||
end
|
||||
|
||||
function serialize.deserialize_inventories(destination, serialized, inventories)
|
||||
for name, index in pairs(inventories) do
|
||||
local inventory = destination.get_inventory(index)
|
||||
if inventory ~= nil and serialized[name] ~= nil then
|
||||
inventory.clear()
|
||||
clusterio_serialize.deserialize_inventory(inventory, serialized[name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Characters are serialized into a table with the following fields:
|
||||
-- character_crafting_speed_modifier
|
||||
-- character_mining_speed_modifier
|
||||
@@ -76,22 +66,198 @@ function serialize.serialize_character(character)
|
||||
return serialized
|
||||
end
|
||||
|
||||
--- Restore the stats of a character, the inventories are restored with place_items
|
||||
function serialize.deserialize_character(character, serialized)
|
||||
-- Deserialize character stats
|
||||
for _, key in pairs(character_stat_keys) do
|
||||
character[key] = serialized[key]
|
||||
end
|
||||
end
|
||||
|
||||
-- Deserialize character inventories
|
||||
serialize.deserialize_inventories(character, serialized.inventories, character_inventories)
|
||||
|
||||
-- Deserialize armor grid state, the grid exists after the armor inventory is restored
|
||||
--- 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
|
||||
if grid and serialized.inhibit_movement_bonus ~= nil then
|
||||
grid.inhibit_movement_bonus = serialized.inhibit_movement_bonus
|
||||
end
|
||||
end
|
||||
|
||||
-- Inventories by the names used in serialized data in the order they are filled, armor first as it adds slots to main
|
||||
local fill_order = { "armor", "guns", "ammo", "main", "trash" }
|
||||
-- Inventories items which do not fit into the inventory they came from are inserted into, in order
|
||||
local overflow_order = { "main", "guns", "ammo", "armor" }
|
||||
|
||||
--- Number of slots a serialized inventory uses
|
||||
--- @param serialized table
|
||||
--- @return number
|
||||
local function serialized_size(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
|
||||
|
||||
--- Copy of a serialized inventory without slot filters, for inventories which do not support them
|
||||
--- @param serialized table
|
||||
--- @return table
|
||||
local function without_filters(serialized)
|
||||
local copy = { i = {} }
|
||||
for _, entry in ipairs(serialized.i) do
|
||||
local item = {}
|
||||
for key, value in pairs(entry) do
|
||||
item[key] = value
|
||||
end
|
||||
item.f = nil
|
||||
table.insert(copy.i, item)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
--- Inventories the items of a player go into, by the names used in serialized data
|
||||
--- @param player LuaPlayer
|
||||
--- @return table<string, LuaInventory>
|
||||
function serialize.item_destinations(player)
|
||||
local destinations = {}
|
||||
local character = player.character
|
||||
if character then
|
||||
for name, index in pairs(character_inventories) do
|
||||
destinations[name] = character.get_inventory(index)
|
||||
end
|
||||
elseif player.controller_type == defines.controllers.god
|
||||
or v2_remote_controller and player.physical_controller_type == defines.controllers.god
|
||||
then
|
||||
destinations.main = player.get_inventory(defines.inventory.god_main) --[[@as LuaInventory]]
|
||||
end
|
||||
return destinations
|
||||
end
|
||||
|
||||
--- Insert as much of a stack as possible, starting with the inventory it came from
|
||||
--- @param stack LuaItemStack
|
||||
--- @param destinations table<string, LuaInventory>
|
||||
--- @param from string?
|
||||
local function insert_stack(stack, destinations, from)
|
||||
local function insert(destination)
|
||||
if destination and stack.valid_for_read then
|
||||
local inserted = destination.insert(stack)
|
||||
if inserted >= stack.count then
|
||||
stack.clear()
|
||||
elseif inserted > 0 then
|
||||
stack.count = stack.count - inserted
|
||||
end
|
||||
end
|
||||
end
|
||||
if from then
|
||||
insert(destinations[from])
|
||||
end
|
||||
for _, name in ipairs(overflow_order) do
|
||||
insert(destinations[name])
|
||||
end
|
||||
end
|
||||
|
||||
--- Replace the items in destinations with the serialized inventories and loose items
|
||||
---
|
||||
--- Each inventory goes slot for slot into the destination of the same name when it fits, otherwise its items are
|
||||
--- inserted where there is space. Returns a script inventory with what did not fit, to be put into a corpse.
|
||||
--- @param destinations table<string, LuaInventory>
|
||||
--- @param inventories table<string, table>?
|
||||
--- @param items { name: string, count: integer, quality: string? }[]?
|
||||
--- @return LuaInventory?
|
||||
function serialize.place_items(destinations, inventories, items)
|
||||
for _, destination in pairs(destinations) do
|
||||
destination.clear()
|
||||
end
|
||||
|
||||
local leftovers = {}
|
||||
for _, name in ipairs(fill_order) do
|
||||
local serialized = inventories and inventories[name]
|
||||
local size = serialized and serialized_size(serialized) or 0
|
||||
local destination = destinations[name]
|
||||
if serialized and destination and size <= #destination then
|
||||
if not destination.supports_filters() then
|
||||
serialized = without_filters(serialized)
|
||||
end
|
||||
clusterio_serialize.deserialize_inventory(destination, serialized)
|
||||
elseif serialized and size > 0 then
|
||||
-- Slot filters belong to the slot they were set on
|
||||
local inventory = game.create_inventory(size)
|
||||
clusterio_serialize.deserialize_inventory(inventory, without_filters(serialized))
|
||||
table.insert(leftovers, { from = name, inventory = inventory })
|
||||
end
|
||||
end
|
||||
|
||||
if items and next(items) then
|
||||
local inventory = game.create_inventory(#items)
|
||||
for _, item in pairs(items) do
|
||||
local remaining = item.count
|
||||
while remaining > 0 do
|
||||
local inserted = inventory.insert({ name = item.name, count = remaining, quality = item.quality })
|
||||
remaining = remaining - inserted
|
||||
if remaining > 0 then
|
||||
if inserted == 0 and not inventory.is_full() then
|
||||
log("ERROR: Unable to place " .. remaining .. " " .. item.name .. ", voiding it")
|
||||
break
|
||||
end
|
||||
inventory.resize(#inventory * 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(leftovers, { inventory = inventory })
|
||||
end
|
||||
|
||||
local spill
|
||||
local spilled = 0
|
||||
for _, leftover in ipairs(leftovers) do
|
||||
local inventory = leftover.inventory
|
||||
for i = 1, #inventory do
|
||||
local stack = inventory[i]
|
||||
if stack.valid_for_read then
|
||||
insert_stack(stack, destinations, leftover.from)
|
||||
end
|
||||
if stack.valid_for_read then
|
||||
spill = spill or game.create_inventory(1)
|
||||
if spilled == #spill then
|
||||
spill.resize(#spill * 2)
|
||||
end
|
||||
spilled = spilled + 1
|
||||
spill[spilled].transfer_stack(stack)
|
||||
end
|
||||
end
|
||||
inventory.destroy()
|
||||
end
|
||||
return spill
|
||||
end
|
||||
|
||||
--- Put items which did not fit into a corpse below the player
|
||||
--- @param player LuaPlayer
|
||||
--- @param spill LuaInventory?
|
||||
function serialize.spill_items(player, spill)
|
||||
if not spill then
|
||||
return
|
||||
end
|
||||
spill.sort_and_merge()
|
||||
local count = #spill - spill.count_empty_stacks()
|
||||
local corpse = player.surface.create_entity({
|
||||
name = "character-corpse",
|
||||
position = player.position,
|
||||
inventory_size = count,
|
||||
player_index = player.index,
|
||||
})
|
||||
if corpse then
|
||||
local inventory = assert(corpse.get_inventory(defines.inventory.character_corpse))
|
||||
for i = 1, #spill do
|
||||
if spill[i].valid_for_read then
|
||||
assert(inventory.find_empty_stack()).transfer_stack(spill[i])
|
||||
end
|
||||
end
|
||||
player.print(
|
||||
"Some of your synced items do not fit into your inventory here and have been placed in a corpse below you."
|
||||
)
|
||||
else
|
||||
log("ERROR: Unable to create a corpse for " .. player.name .. ", voiding " .. count .. " stacks of items")
|
||||
end
|
||||
spill.destroy()
|
||||
end
|
||||
|
||||
-- Personal logistic slots is a table mapping string indexes to a table with the following fields:
|
||||
-- name
|
||||
-- min
|
||||
@@ -857,18 +1023,17 @@ function serialize.serialize_player(player, failed_deserialization, components)
|
||||
end
|
||||
|
||||
-- For the waiting to respawn state the inventory logistic requests and filters are hidden on the player
|
||||
if sync_inventories and player.controller_type == defines.controllers.ghost and player.ticks_to_respawn then
|
||||
if (sync_inventories or sync_logistics)
|
||||
and player.controller_type == defines.controllers.ghost and player.ticks_to_respawn
|
||||
then
|
||||
local ticks_to_respawn = player.ticks_to_respawn
|
||||
player.ticks_to_respawn = nil -- Respawn now
|
||||
|
||||
if sync_logistics then
|
||||
serialized.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
||||
end
|
||||
if sync_controller then
|
||||
if sync_inventories then
|
||||
serialized.inventories = serialize.serialize_inventories(player, character_inventories)
|
||||
elseif player.character then
|
||||
-- Without the controller the death is synced by uploading the respawned character
|
||||
serialized.character = serialize.serialize_character(player.character)
|
||||
end
|
||||
|
||||
-- Go back to waiting for respawn
|
||||
@@ -890,7 +1055,7 @@ function serialize.serialize_player(player, failed_deserialization, components)
|
||||
end
|
||||
|
||||
-- Serialize non-character inventories
|
||||
if sync_controller and sync_inventories and (
|
||||
if sync_inventories and (
|
||||
player.controller_type == defines.controllers.god
|
||||
or v2_remote_controller and player.physical_controller_type == defines.controllers.god
|
||||
) then
|
||||
@@ -994,7 +1159,7 @@ end
|
||||
--- @param serialized SerializedPlayerData
|
||||
--- @param components SyncComponents
|
||||
--- @param local_controller LocalControllerState? Controller to restore when the controller is not synced
|
||||
--- @return FailedDeserializationPlayerData?
|
||||
--- @return FailedDeserializationPlayerData?, LuaInventory? Items which did not fit, see spill_items
|
||||
function serialize.deserialize_player(player, serialized, components, local_controller)
|
||||
local failed_deserialization = {}
|
||||
local sync_controller = syncs(components, "controller")
|
||||
@@ -1008,15 +1173,18 @@ function serialize.deserialize_player(player, serialized, components, local_cont
|
||||
local state = local_controller
|
||||
if restore_controller then
|
||||
state = { controller = serialized.controller, ticks_to_respawn = serialized.ticks_to_respawn }
|
||||
elseif state and state.controller == "ghost" and sync_inventories and serialized.character then
|
||||
-- Respawn now, a ghost would drop the synced character and upload an empty one after respawning
|
||||
state = { controller = "character" }
|
||||
end
|
||||
|
||||
-- Items can come from a character, a god or a ghost and go to whatever controller the player ends up with
|
||||
local inventories = serialized.character and serialized.character.inventories or serialized.inventories
|
||||
local queue = serialized.crafting_queue
|
||||
local spill
|
||||
|
||||
local target_controller = state and defines.controllers[state.controller]
|
||||
local to_ghost = state and state.controller == "ghost"
|
||||
if state and (
|
||||
player.controller_type ~= target_controller
|
||||
or restore_controller and state.controller == "ghost"
|
||||
or to_ghost and (restore_controller or sync_inventories or sync_logistics)
|
||||
) then
|
||||
if state.controller == "character" then
|
||||
-- Create a character but do not destroy an existing one
|
||||
@@ -1030,11 +1198,13 @@ function serialize.deserialize_player(player, serialized, components, local_cont
|
||||
-- The force first, so the character and the logistic groups are on the restored player's force
|
||||
player.force = serialized.force
|
||||
local character = ensure_character(player)
|
||||
if restore_controller and sync_logistics and serialized.personal_logistic_slots then
|
||||
if sync_logistics and serialized.personal_logistic_slots then
|
||||
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
||||
end
|
||||
if restore_controller and sync_inventories and serialized.inventories then
|
||||
serialize.deserialize_inventories(player, serialized.inventories, character_inventories)
|
||||
if sync_inventories then
|
||||
spill = serialize.place_items(
|
||||
serialize.item_destinations(player), inventories, queue and queue.ingredients
|
||||
)
|
||||
end
|
||||
if state.ticks_to_respawn then
|
||||
player.ticks_to_respawn = state.ticks_to_respawn
|
||||
@@ -1089,21 +1259,26 @@ function serialize.deserialize_player(player, serialized, components, local_cont
|
||||
end
|
||||
end
|
||||
|
||||
-- Deserialize character, the stored player may not have had one when the controller is not synced
|
||||
if player.character then
|
||||
if sync_inventories and serialized.character then
|
||||
serialize.deserialize_character(player.character, serialized.character)
|
||||
-- Deserialize items, a ghost got them while it had a character above
|
||||
local character = player.character
|
||||
if sync_inventories and not to_ghost then
|
||||
local stats = character and serialized.character
|
||||
if stats then
|
||||
serialize.deserialize_character(character, stats)
|
||||
end
|
||||
if sync_logistics then
|
||||
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
||||
local craft = character and queue
|
||||
spill = serialize.place_items(
|
||||
serialize.item_destinations(player), inventories, not craft and queue and queue.ingredients or nil
|
||||
)
|
||||
if stats then
|
||||
serialize.deserialize_character_grid(character, stats)
|
||||
end
|
||||
if craft then
|
||||
serialize.deserialize_crafting_queue(player, queue)
|
||||
end
|
||||
end
|
||||
|
||||
-- Deserialize non-character inventories
|
||||
if restore_controller and sync_inventories and serialized.inventories
|
||||
and player.controller_type == defines.controllers.god
|
||||
then
|
||||
serialize.deserialize_inventories(player, serialized.inventories, { main = defines.inventory.god_main })
|
||||
if character and sync_logistics then
|
||||
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
||||
end
|
||||
|
||||
-- Deserialize quick bar (named hotbar in old data)
|
||||
@@ -1111,18 +1286,13 @@ function serialize.deserialize_player(player, serialized, components, local_cont
|
||||
serialize.deserialize_quick_bar(player, serialized.quick_bar or serialized.hotbar)
|
||||
end
|
||||
|
||||
-- Deserialize crafting queue
|
||||
if sync_inventories and player.character and serialized.crafting_queue then
|
||||
serialize.deserialize_crafting_queue(player, serialized.crafting_queue)
|
||||
end
|
||||
|
||||
-- Deserialize recipe notifications
|
||||
if recipe_notifications_api and syncs(components, "recipe_notifications") and serialized.recipe_notifications then
|
||||
failed_deserialization.recipe_notifications =
|
||||
serialize.deserialize_crafting_notifications(player, serialized.recipe_notifications)
|
||||
end
|
||||
|
||||
return next(failed_deserialization) and failed_deserialization or nil
|
||||
return next(failed_deserialization) and failed_deserialization or nil, spill
|
||||
end
|
||||
|
||||
return serialize
|
||||
Reference in new issue
Block a user