mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
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>
311 lines
9.2 KiB
Lua
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
|