Files
bbassieandClaude Opus 5.5 a1a57b8c97 Place synced items from a list of stacks instead of script inventories
place_items no longer creates script inventories for what does not fit. Each serialized inventory is split at the size of its destination: the slots that fit are restored slot for slot and the rest become serialized stacks, which are inserted into the free space of the same inventory and then main, guns, ammo and armor. What is still left over is returned as a list and spill_items puts it into a corpse sized to hold it. This keeps 1.0 out of reach of game.create_inventory.

The armor-first fill order is gone, as the bonus slots from armor equipment only apply on the next tick anyway (#1106). Filters are no longer stripped for destinations, they are the same inventory type the data came from.

deserialize_inventory concatenated the filter table in its error print on 2.0, which errored out the whole restore when an inventory did not support filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:36:22 +00:00

311 lines
9.2 KiB
Lua

-- Library for serializing items in Factorio
-- Based on code from playerManager and trainTeleports
local compat = require("modules/clusterio/compat")
local serialize = {}
-- 0.17 compatibility
local supports_bar, get_bar, set_bar
if compat.version_ge("1.0.0") then
supports_bar = "supports_bar"
get_bar = "get_bar"
set_bar = "set_bar"
else
supports_bar = "hasbar"
get_bar = "getbar"
set_bar = "setbar"
end
local has_create_grid = compat.version_ge("1.1.7")
local has_quality = compat.version_ge("2.0.0")
local has_spoilage = compat.version_ge("2.0.0")
-- Equipment Grids are serialized into an array of equipment entries
-- where ench entry is a table with the following fields:
-- n: name
-- g: ghost name (optional)
-- t: to_be_removed (optional)
-- p: position (array of 2 numbers corresponding to x and y)
-- s: shield (optional)
-- e: energy (optional)
-- If the equipment is a burner the following is also present:
-- i: burner inventory
-- r: result inventory
-- b: currently burning (optional)
-- f: remaining_burning_fuel (optional)
-- If the equipment has a quality the following is also present:
-- q: quality (optional)
function serialize.serialize_equipment_grid(grid)
local serialized = {}
local processed = {}
for y = 0, grid.height - 1 do
for x = 0, grid.width - 1 do
local equipment = grid.get({x, y})
if equipment ~= nil then
local pos = equipment.position
local combined_pos = pos.x + pos.y * grid.width + 1
if not processed[combined_pos] then
processed[combined_pos] = true
local entry = {
n = equipment.name,
p = {pos.x, pos.y},
}
if equipment.type == "equipment-ghost" then
if equipment.ghost_name then
entry.g = equipment.ghost_name
end
end
if equipment.shield > 0 then entry.s = equipment.shield end
if equipment.energy > 0 then entry.e = equipment.energy end
if equipment.burner then
local burner = equipment.burner
entry.i = serialize.serialize_inventory(burner.inventory)
entry.r = serialize.serialize_inventory(burner.burnt_result_inventory)
if burner.currently_burning then
entry.b = burner.currently_burning.name
entry.f = burner.remaining_burning_fuel
end
end
if equipment.quality then
entry.q = equipment.quality.name
end
if equipment.to_be_removed then
entry.t = equipment.to_be_removed
end
table.insert(serialized, entry)
end
end
end
end
return serialized
end
function serialize.deserialize_equipment_grid(grid, serialized)
grid.clear()
for _, entry in ipairs(serialized) do
local equipment = grid.put({
name = entry.g or entry.n,
quality = has_quality and entry.q or nil,
position = entry.p,
ghost = entry.g and true or nil,
})
if equipment then
if entry.s then equipment.shield = entry.s end
if entry.e then equipment.energy = entry.e end
if entry.i then
local burner = equipment.burner
if entry.b then burner.currently_burning = entry.b end
if entry.f then burner.remaining_burning_fuel = entry.f end
serialize.deserialize_inventory(burner.burnt_result_inventory, entry.r)
serialize.deserialize_inventory(burner.inventory, entry.i)
end
end
end
end
-- Item stacks are serialized into a table with the following fields:
-- n: name
-- c: count
-- q: quality (optional)
-- h: health (optional)
-- p: spoil percent (optional)
-- d: durability (optional)
-- a: ammo count (optional)
-- l: label (optional)
-- g: equipment grid (optional)
-- i: item inventory (optional)
-- If the item stack is exportable it has the following property instead
-- e: export string
-- Label is a table with the following fields:
-- t: label text (optional)
-- c: color (optional)
-- a: allow manual label change
function serialize.serialize_item_stack(slot, entry)
if
slot.is_blueprint
or slot.is_blueprint_book
or slot.is_upgrade_item
or slot.is_deconstruction_item
or slot.is_item_with_tags
then
local call_success, call_return = pcall(slot.export_stack)
if not call_success then
print("Error: '" .. call_return .. "' thrown exporting '" .. slot.name .. "'")
else
entry.e = call_return
end
return
end
entry.n = slot.name
entry.c = slot.count
if has_quality and slot.quality.level ~= 0 then entry.q = slot.quality.name end
if slot.health < 1 then entry.h = slot.health end
if has_spoilage and slot.spoil_percent > 0 then entry.p = slot.spoil_percent end
if slot.type == "tool" and slot.durability then entry.d = slot.durability end
if slot.type == "ammo" then entry.a = slot.ammo end
if slot.is_item_with_label then
local label = {}
if slot.label then label.t = slot.label end
if slot.label_color then label.c = slot.label_color end
label.a = slot.allow_manual_label_change
entry.l = label
end
if slot.grid then
entry.g = serialize.serialize_equipment_grid(slot.grid)
end
if slot.is_item_with_inventory then
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
entry.i = serialize.serialize_inventory(sub_inventory)
end
end
function serialize.deserialize_item_stack(slot, entry)
if entry.e then
local success = slot.import_stack(entry.e)
if success == 1 then
print("Error: import of '" .. entry.e .. "' succeeded with errors")
elseif success == -1 then
print("Error: import of '" .. entry.e .. "' failed")
end
return
end
local item_stack = {
name = entry.n,
count = entry.c,
}
if entry.q then item_stack.quality = entry.q end
if entry.h then item_stack.health = entry.h end
if entry.p then item_stack.spoil_percent = entry.p end
if entry.d then item_stack.durability = entry.d end
if entry.a then item_stack.ammo = entry.a end
local call_success, call_return = pcall(slot.set_stack, item_stack)
if not call_success then
print("Error: '" .. call_return .. "' thrown setting stack ".. serpent.line(entry))
elseif not call_return then
print("Error: Failed to set stack " .. serpent.line(entry))
else
if entry.l then
-- TODO test this with AAI's unit-remote-control
local label = entry.l
if label.t then slot.label = label.t end
if label.c then slot.label_color = label.c end
slot.allow_manual_label_change = label.a
end
if entry.g then
if slot.grid then
serialize.deserialize_equipment_grid(slot.grid, entry.g)
elseif slot.type == "item-with-entity-data" and has_create_grid then
slot.create_grid()
serialize.deserialize_equipment_grid(slot.grid, entry.g)
else
print("Error: Attempt to deserialize equipment grid on an unsupported entity")
end
end
if entry.i then
local sub_inventory = slot.get_inventory(defines.inventory.item_main)
serialize.deserialize_inventory(sub_inventory, entry.i)
end
end
end
-- Inventories are serialized into a table with the following fields:
-- i: array of item stack or exportable item entries
-- b: bar position (optional)
-- Each item entry has the following fields
-- s: index (optional, equals to previous plus one if not present)
-- r: repeat count (optional)
-- f: slot filter (optional)
-- Pluss all the fields for item stacks (see deserialize_item_stack)
-- It's also possible that the slot is empty but has a slot filter.
function serialize.serialize_inventory(inventory)
local serialized = {}
if inventory[supports_bar]() and inventory[get_bar]() <= #inventory then
serialized.b = inventory[get_bar]()
end
serialized.i = {}
local previous_index = 0
local previous_serialized = nil
for i = 1, #inventory do
local item = {}
local slot = inventory[i]
if inventory.supports_filters() then
item.f = inventory.get_filter(i)
end
if slot.valid_for_read then
serialize.serialize_item_stack(slot, item)
end
if item.n or item.f or item.e then
local item_serialized = compat.table_to_json(item)
if item_serialized == previous_serialized then
local previous_item = serialized.i[#serialized.i]
previous_item.r = (previous_item.r or 0) + 1
previous_index = i
else
if i ~= previous_index + 1 then
item.s = i
end
previous_index = i
previous_serialized = item_serialized
table.insert(serialized.i, item)
end
else
-- Either an empty slot or serilization failed
previous_index = 0
previous_serialized = nil
end
end
return serialized
end
function serialize.deserialize_inventory(inventory, serialized)
if serialized.b and inventory[supports_bar]() then
inventory[set_bar](serialized.b)
end
local last_slot_index = 0
for _, entry in ipairs(serialized.i) do
local base_index = entry.s or last_slot_index + 1
local repeat_count = entry.r or 0
for offset = 0, repeat_count do
local index = base_index + offset
if index > #inventory then
print("Error: Inventory index out of bounds deserializing inventory, voiding excess slots")
break
end
local slot = inventory[index]
if entry.f then
local call_success, call_return = pcall(inventory.set_filter, index, entry.f)
if not call_success then
print("Error: '" .. call_return .. "' thrown setting filter " .. serpent.line(entry.f))
elseif not call_return then
print("Error: Failed to set filter " .. serpent.line(entry.f))
end
end
if entry.n or entry.e then
serialize.deserialize_item_stack(slot, entry)
end
end
last_slot_index = base_index + repeat_count
end
end
return serialize