mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
serialize_local_controller now respawns a player waiting to respawn for a moment to read the logistic requests hidden in the ghost, when logistics are not synced. Without this the requests were lost when the download turned the ghost into a spectator. deserialize_player compares the physical controller with the one it restores. A player in remote view with a character was seen as changing controller, so with inventories off their crafting queue was serialized and queued a second time on the same character. The ghost branch only sets the force when the force is synced, it errored on the stripped field otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1361 lines
44 KiB
Lua
1361 lines
44 KiB
Lua
local compat = require("modules/clusterio/compat")
|
|
local clusterio_serialize = require("modules/clusterio/serialize")
|
|
local character_inventories = require("modules/player_sync/define_player_inventories")
|
|
local character_stat_keys = require("modules/player_sync/define_player_stat_keys")
|
|
local game_view_settings_keys = require("modules/player_sync/define_game_view_settings")
|
|
local serialize = {}
|
|
|
|
local v2_logistic_api = compat.version_ge("2.0.0")
|
|
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 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")
|
|
|
|
function serialize.serialize_inventories(source, inventories)
|
|
local serialized = {}
|
|
|
|
for name, index in pairs(inventories) do
|
|
local inventory = source.get_inventory(index)
|
|
if inventory ~= nil then
|
|
serialized[name] = clusterio_serialize.serialize_inventory(inventory)
|
|
end
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
-- Characters are serialized into a table with the following fields:
|
|
-- character_crafting_speed_modifier
|
|
-- character_mining_speed_modifier
|
|
-- character_additional_mining_categories
|
|
-- character_running_speed_modifier
|
|
-- character_build_distance_bonus
|
|
-- character_item_drop_distance_bonus
|
|
-- character_reach_distance_bonus
|
|
-- character_resource_reach_distance_bonus
|
|
-- character_item_pickup_distance_bonus
|
|
-- character_loot_pickup_distance_bonus
|
|
-- character_inventory_slots_bonus
|
|
-- character_trash_slot_count_bonus
|
|
-- character_maximum_following_robot_count_bonus
|
|
-- character_health_bonus
|
|
-- character_personal_logistic_requests_enabled
|
|
-- allow_dispatching_robots
|
|
-- inhibit_movement_bonus (optional, from the armor equipment grid)
|
|
-- inventories: table of character inventory name to inventory content
|
|
function serialize.serialize_character(character)
|
|
local serialized = { }
|
|
|
|
-- Serialize character stats
|
|
for _, key in pairs(character_stat_keys) do
|
|
serialized[key] = character[key]
|
|
end
|
|
|
|
-- Serialize character inventories
|
|
serialized.inventories = serialize.serialize_inventories(character, character_inventories)
|
|
|
|
-- Serialize armor grid state
|
|
local grid = character.grid
|
|
if grid then
|
|
serialized.inhibit_movement_bonus = grid.inhibit_movement_bonus
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
--- Restore the stats of a character, the inventories are restored with place_items
|
|
function serialize.deserialize_character(character, serialized)
|
|
for _, key in pairs(character_stat_keys) do
|
|
character[key] = serialized[key]
|
|
end
|
|
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
|
|
if grid and serialized.inhibit_movement_bonus ~= nil then
|
|
grid.inhibit_movement_bonus = serialized.inhibit_movement_bonus
|
|
end
|
|
end
|
|
|
|
-- Inventories items which do not fit into the inventory they came from are inserted into, in order
|
|
local overflow_order = { "main", "guns", "ammo", "armor" }
|
|
|
|
--- Copy of a serialized item entry without the slot index, repeat count and filter
|
|
--- @param entry table
|
|
--- @return table
|
|
local function stack_entry(entry)
|
|
local stack = {}
|
|
for key, value in pairs(entry) do
|
|
stack[key] = value
|
|
end
|
|
stack.s = nil
|
|
stack.r = nil
|
|
stack.f = nil
|
|
return stack
|
|
end
|
|
|
|
--- Split a serialized inventory into the slots which fit into an inventory of size and the stacks of those which do not
|
|
--- @param serialized table
|
|
--- @param size number
|
|
--- @return table fitting, table[] rest
|
|
local function split_inventory(serialized, size)
|
|
local fitting = { b = serialized.b, i = {} }
|
|
local rest = {}
|
|
local last = 0
|
|
for _, entry in ipairs(serialized.i) do
|
|
local first = entry.s or last + 1
|
|
last = first + (entry.r or 0)
|
|
if last <= size then
|
|
table.insert(fitting.i, entry)
|
|
else
|
|
if first <= size then
|
|
local part = stack_entry(entry)
|
|
part.s = first
|
|
part.r = size > first and size - first or nil
|
|
part.f = entry.f
|
|
table.insert(fitting.i, part)
|
|
end
|
|
if entry.n or entry.e then
|
|
for _ = math.max(first, size + 1), last do
|
|
table.insert(rest, stack_entry(entry))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return fitting, rest
|
|
end
|
|
|
|
local stack_definition_keys = { n = true, c = true, q = true, h = true, p = true, d = true, a = true }
|
|
|
|
--- Insert as much of a serialized stack as possible into an inventory, reducing its count by what was inserted
|
|
--- @param inventory LuaInventory
|
|
--- @param stack table
|
|
--- @return boolean placed true if all of it was inserted
|
|
local function insert_stack(inventory, stack)
|
|
local plain = true
|
|
for key in pairs(stack) do
|
|
plain = plain and stack_definition_keys[key] or false
|
|
end
|
|
|
|
-- Stacks without labels, grids, inventories or export strings can be merged with what is in the inventory
|
|
if plain then
|
|
local inserted = inventory.insert({
|
|
name = stack.n, count = stack.c, quality = stack.q, health = stack.h,
|
|
spoil_percent = stack.p, durability = stack.d, ammo = stack.a,
|
|
})
|
|
stack.c = stack.c - inserted
|
|
return stack.c <= 0
|
|
end
|
|
|
|
-- Export strings have no name, and 1.1 does not take nil for it
|
|
local slot
|
|
if stack.n then
|
|
slot = inventory.find_empty_stack(stack.n)
|
|
else
|
|
slot = inventory.find_empty_stack()
|
|
end
|
|
if not slot or stack.n and not slot.can_set_stack({ name = stack.n, count = stack.c, quality = stack.q }) then
|
|
return false
|
|
end
|
|
clusterio_serialize.deserialize_item_stack(slot, stack)
|
|
return slot.valid_for_read
|
|
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
|
|
|
|
--- 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 as far as it fits, the slots past its
|
|
--- size are inserted where there is space. Returns the serialized stacks which did not fit, see spill_items.
|
|
--- @param destinations table<string, LuaInventory>
|
|
--- @param inventories table<string, table>?
|
|
--- @param items { name: string, count: integer, quality: string? }[]?
|
|
--- @return table[] leftovers
|
|
function serialize.place_items(destinations, inventories, items)
|
|
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
|
|
local pending = {}
|
|
for name, serialized in pairs(inventories or {}) do
|
|
local destination = destinations[name]
|
|
local fitting, rest = split_inventory(serialized, destination and #destination or 0)
|
|
if destination then
|
|
clusterio_serialize.deserialize_inventory(destination, fitting)
|
|
end
|
|
for _, stack in ipairs(rest) do
|
|
table.insert(pending, { from = destination, stack = stack })
|
|
end
|
|
end
|
|
for _, item in pairs(items or {}) do
|
|
table.insert(pending, { stack = { n = item.name, c = item.count, q = item.quality } })
|
|
end
|
|
|
|
local leftovers = {}
|
|
for _, entry in ipairs(pending) do
|
|
local stack = entry.stack
|
|
local placed = entry.from and insert_stack(entry.from, stack)
|
|
for _, name in ipairs(overflow_order) do
|
|
placed = placed or destinations[name] and insert_stack(destinations[name], stack)
|
|
end
|
|
if not placed then
|
|
table.insert(leftovers, stack)
|
|
end
|
|
end
|
|
return leftovers
|
|
end
|
|
|
|
--- Put serialized stacks which did not fit into a corpse below the player
|
|
--- @param player LuaPlayer
|
|
--- @param stacks table[]?
|
|
function serialize.spill_items(player, stacks)
|
|
if not stacks or not next(stacks) then
|
|
return
|
|
end
|
|
|
|
-- Plain stacks can hold more than a stack's worth
|
|
local slots = 0
|
|
for _, stack in ipairs(stacks) do
|
|
local prototype = stack.n and compat.prototypes.item[stack.n]
|
|
slots = slots + (prototype and math.ceil((stack.c or 1) / prototype.stack_size) or 1)
|
|
end
|
|
|
|
local corpse = player.surface.create_entity({
|
|
name = "character-corpse",
|
|
position = player.position,
|
|
inventory_size = slots,
|
|
player_index = player.index,
|
|
})
|
|
if not corpse then
|
|
log("ERROR: Unable to create a corpse for " .. player.name .. ", voiding " .. #stacks .. " stacks of items")
|
|
return
|
|
end
|
|
|
|
local inventory = assert(corpse.get_inventory(defines.inventory.character_corpse))
|
|
for _, stack in ipairs(stacks) do
|
|
if not insert_stack(inventory, stack) then
|
|
log("ERROR: Unable to place " .. (stack.n or "an exported item") .. " into the corpse of " .. player.name)
|
|
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."
|
|
)
|
|
end
|
|
|
|
-- Personal logistic slots is a table mapping string indexes to a table with the following fields:
|
|
-- name
|
|
-- min
|
|
-- max
|
|
function serialize.serialize_personal_logistic_slots(player)
|
|
-- Check if logistics technology is researched
|
|
local force = player.force
|
|
if not force.technologies["logistic-robotics"] or not force.technologies["logistic-robotics"].researched then
|
|
return nil
|
|
end
|
|
|
|
if v2_logistic_api then
|
|
local logistic_point = player.get_requester_point()
|
|
if logistic_point == nil then
|
|
return nil
|
|
end
|
|
local serialized = {
|
|
enabled = logistic_point.enabled,
|
|
trash_not_requested = logistic_point.trash_not_requested,
|
|
sections = {},
|
|
}
|
|
for i = 1, logistic_point.sections_count do
|
|
local section = logistic_point.get_section(i)
|
|
serialized.sections[i] = {
|
|
group = section.group,
|
|
active = section.active,
|
|
multiplier = section.multiplier,
|
|
filters = section.filters,
|
|
}
|
|
end
|
|
return serialized
|
|
end
|
|
|
|
-- Empty when there are no requests, nil means the player has nowhere to hold them
|
|
local serialized = {}
|
|
|
|
-- Serialize personal logistic slots
|
|
local last_valid = 0
|
|
for i = 1, 65536 do
|
|
local slot = player.get_personal_logistic_slot(i)
|
|
if slot.name then
|
|
last_valid = i
|
|
serialized[tostring(i)] = {
|
|
name = slot.name,
|
|
min = slot.min,
|
|
max = slot.max,
|
|
}
|
|
|
|
-- Stop after 100 empty slots
|
|
elseif last_valid + 100 <= i then
|
|
break
|
|
end
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
function serialize.deserialize_personal_logistic_slots(player, serialized)
|
|
if not serialized then
|
|
return
|
|
end
|
|
|
|
-- Check if logistics technology is researched
|
|
local force = player.force
|
|
if not force.technologies["logistic-robotics"] or not force.technologies["logistic-robotics"].researched then
|
|
return
|
|
end
|
|
|
|
-- Load personal logistic slots
|
|
if v2_logistic_api then
|
|
local logistic_point = player.get_requester_point()
|
|
if logistic_point == nil then
|
|
return
|
|
end
|
|
|
|
-- Remove old sections up to section_count
|
|
for i = logistic_point.sections_count, 1, -1 do
|
|
logistic_point.remove_section(i)
|
|
end
|
|
|
|
-- If this is an array instead of a table, migrate to v2 format
|
|
if next(serialized) == nil then
|
|
return
|
|
elseif serialized[1] ~= nil then
|
|
local section = logistic_point.add_section()
|
|
for i, slot in pairs(serialized) do
|
|
section.set_slot(i, {
|
|
value = {
|
|
name = slot.name,
|
|
quality = "normal",
|
|
},
|
|
min = slot.min,
|
|
max = slot.max,
|
|
})
|
|
end
|
|
else
|
|
-- Regular 2.0+ deserialization
|
|
logistic_point.enabled = serialized.enabled
|
|
logistic_point.trash_not_requested = serialized.trash_not_requested
|
|
for i, section in pairs(serialized.sections) do
|
|
local sec = logistic_point.add_section()
|
|
sec.active = section.active
|
|
sec.multiplier = section.multiplier
|
|
if section.group ~= "" then -- "" is the default group name, which is truthy
|
|
-- Named groups that have filters on the server get added with name only - this avoids
|
|
-- overwriting existing groups. Missing or empty groups get the filters of the player.
|
|
local existing = force.get_logistic_group(section.group)
|
|
sec.group = section.group
|
|
if not existing or #existing.filters == 0 then
|
|
sec.filters = section.filters
|
|
end
|
|
else
|
|
-- Unnamed groups get added with filters
|
|
sec.filters = section.filters
|
|
end
|
|
end
|
|
end
|
|
else
|
|
local last_valid = 0
|
|
for i = 1, 65536 do
|
|
if player.get_personal_logistic_slot(i).name then
|
|
last_valid = i
|
|
player.clear_personal_logistic_slot(i)
|
|
elseif last_valid + 100 <= i then
|
|
break
|
|
end
|
|
end
|
|
for i, slot in pairs(serialized) do
|
|
player.set_personal_logistic_slot(tonumber(i), slot)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- name is a custom type where quality is "normal" and comparator is "=" (most common case)
|
|
--- @alias QuickBarSlotEncoded.name string
|
|
--- @alias QuickBarSlotEncoded.filter { t: "f", n: string, q: string, c: string }
|
|
--- b is the slot indices inside blueprint books, from the book in slot s down to the item, absent for a top-level item
|
|
--- @alias QuickBarSlotEncoded.item { t: "i", n: string, s: integer, b: integer[]? }
|
|
--- @alias QuickBarSlotEncoded.record { t: "r", p: integer[], y: string, l: string? }
|
|
--- @alias QuickBarSlotEncoded QuickBarSlotEncoded.name | QuickBarSlotEncoded.filter | QuickBarSlotEncoded.item | QuickBarSlotEncoded.record
|
|
|
|
--- Inventories of the player that can hold the item a quick bar slot points at
|
|
--- @param player LuaPlayer
|
|
local function get_quick_bar_inventories(player)
|
|
return player.character and character_inventories or { main = defines.inventory.god_main }
|
|
end
|
|
|
|
--- Path of indices from the player's blueprint library down through books to the record
|
|
--- @param records table<integer, LuaRecord>
|
|
--- @param target LuaRecord
|
|
--- @return integer[]?
|
|
local function find_record_path(records, target)
|
|
for index, record in pairs(records) do
|
|
if record == target then
|
|
return { index }
|
|
end
|
|
if record.valid and record.type == "blueprint-book" then
|
|
local path = find_record_path(record.contents, target)
|
|
if path then
|
|
table.insert(path, 1, index)
|
|
return path
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Path of slot indices to the item with the item number, descending into blueprint books
|
|
--- @param inventory LuaInventory
|
|
--- @param item_number integer
|
|
--- @return integer[]?
|
|
local function find_item_path(inventory, item_number)
|
|
for i = 1, #inventory do
|
|
local stack = inventory[i] --[[@as LuaItemStack]]
|
|
if stack.valid_for_read then
|
|
if stack.item_number == item_number then
|
|
return { i }
|
|
end
|
|
local inner = stack.is_blueprint_book and stack.get_inventory(defines.inventory.item_main)
|
|
local path = inner and find_item_path(inner, item_number)
|
|
if path then
|
|
table.insert(path, 1, i)
|
|
return path
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- @param filter ItemFilter
|
|
--- @return QuickBarSlotEncoded
|
|
local function serialize_filter(filter)
|
|
if filter.quality == "normal" and filter.comparator == "=" then
|
|
return filter.name --[[@as string]]
|
|
end
|
|
|
|
return {
|
|
t = "f",
|
|
n = filter.name --[[@as string]],
|
|
q = filter.quality --[[@as string]],
|
|
c = filter.comparator --[[@as string]],
|
|
}
|
|
end
|
|
|
|
--- @param slot QuickBarSlot | ItemFilter | LuaItemPrototype
|
|
--- @param player LuaPlayer
|
|
--- @return QuickBarSlotEncoded | nil
|
|
function serialize.serialize_quick_bar_slot(slot, player)
|
|
if v2_1_quick_bar_api then
|
|
-- 2.1 has a dedicated type for quick bar slots
|
|
--- @cast slot QuickBarSlot
|
|
if slot.type == "filter" then
|
|
return serialize_filter(slot.filter)
|
|
end
|
|
|
|
if slot.type == "item" then
|
|
-- A specific item (blueprint, spidertron remote) is stored as the inventory slot that holds it
|
|
local item_number = slot.item --[[@as LuaItem]].item_number
|
|
for name, inventory_id in pairs(get_quick_bar_inventories(player)) do
|
|
local inventory = player.get_inventory(inventory_id)
|
|
local path = inventory and find_item_path(inventory, item_number)
|
|
if path then
|
|
local top = table.remove(path, 1)
|
|
return { t = "i", n = name, s = top, b = path[1] and path or nil }
|
|
end
|
|
end
|
|
|
|
print("Warning: Could not find the item of quick bar slot of type 'item' in the inventories")
|
|
return nil
|
|
end
|
|
|
|
if slot.type == "record" then
|
|
-- ponytail: one library walk per record slot per upload, a huge library makes the leave tick longer
|
|
local record = slot.record --[[@as LuaRecord]]
|
|
local path = find_record_path(player.blueprints, record)
|
|
if path then
|
|
return { t = "r", p = path, y = record.type, l = record.label }
|
|
end
|
|
|
|
print("Warning: Quick bar slot record is not in the player's blueprint library")
|
|
return nil
|
|
end
|
|
|
|
print("Warning: Unsupported quick bar slot type '" .. slot.type .. "'")
|
|
return nil
|
|
end
|
|
|
|
if v2_0_quick_bar_api then
|
|
-- 2.0 gives us an item filter which supports quality
|
|
--- @cast slot -QuickBarSlot, -LuaItemPrototype
|
|
return serialize_filter(slot)
|
|
end
|
|
|
|
-- pre 2.0 quality did not exist
|
|
--- @cast slot LuaItemPrototype
|
|
return slot.name
|
|
end
|
|
|
|
--- @param entry string | QuickBarSlotEncoded
|
|
--- @param player LuaPlayer
|
|
--- @return QuickBarSlot | ItemWithQualityID | LuaItemStack | nil
|
|
function serialize.deserialize_quick_bar_slot(entry, player)
|
|
if v2_1_quick_bar_api then
|
|
-- Return a quick bar slot
|
|
if type(entry) == "string" then
|
|
return {
|
|
type = "filter",
|
|
filter = {
|
|
name = entry,
|
|
quality = "normal",
|
|
comparator = "=",
|
|
},
|
|
}
|
|
end
|
|
|
|
if entry.t == "f" then
|
|
return {
|
|
type = "filter",
|
|
filter = {
|
|
name = entry.n,
|
|
quality = entry.q,
|
|
comparator = entry.c,
|
|
},
|
|
} --[[@as QuickBarSlot]]
|
|
end
|
|
|
|
if entry.t == "i" then
|
|
-- The inventories have been restored, the stack points the slot at the item in it
|
|
local inventory_id = get_quick_bar_inventories(player)[entry.n]
|
|
local inventory = inventory_id and player.get_inventory(inventory_id)
|
|
local stack = inventory and entry.s <= #inventory and inventory[entry.s] or nil
|
|
-- Walk down through the books to a stack kept inside them
|
|
for _, index in ipairs(entry.b or {}) do
|
|
local inner = stack and stack.valid_for_read and stack.is_blueprint_book
|
|
and stack.get_inventory(defines.inventory.item_main)
|
|
stack = inner and index <= #inner and inner[index] or nil
|
|
end
|
|
if stack and stack.valid_for_read then
|
|
return stack
|
|
end
|
|
|
|
print("Warning: Could not find the item of quick bar slot in " .. entry.n .. " slot " .. entry.s)
|
|
return nil
|
|
end
|
|
|
|
if entry.t == "r" then
|
|
-- The record must still be what was stored, a shifted library must not set the wrong one
|
|
local records = player.blueprints
|
|
local record --[[@as LuaRecord?]]
|
|
for _, index in ipairs(entry.p) do
|
|
record = records[index]
|
|
if not (record and record.valid) then
|
|
record = nil
|
|
break
|
|
end
|
|
records = record.type == "blueprint-book" and record.contents or {}
|
|
end
|
|
if record and record.valid and record.type == entry.y and (record.label or "") == (entry.l or "") then
|
|
return { type = "record", record = record } --[[@as QuickBarSlot]]
|
|
end
|
|
|
|
print("Warning: Could not find the record of quick bar slot at library path " .. table.concat(entry.p, "/"))
|
|
return nil
|
|
end
|
|
|
|
print("Warning: Unsupported serialized quick bar slot type '" .. tostring(entry.t) .. "'")
|
|
return nil
|
|
end
|
|
|
|
if v2_0_quick_bar_api then
|
|
-- Return a ItemIDAndQualityIDPair (member of ItemWithQualityID)
|
|
if type(entry) == "string" then
|
|
return {
|
|
name = entry,
|
|
quality = "normal",
|
|
}
|
|
end
|
|
|
|
-- 2.1 entries (items, records) can not be represented on 2.0, clearing the slot
|
|
if entry.t ~= "f" then
|
|
return nil
|
|
end
|
|
--- @cast entry QuickBarSlotEncoded.filter
|
|
return {
|
|
name = entry.n,
|
|
quality = entry.q,
|
|
}
|
|
end
|
|
|
|
-- Return a string (member of ItemPrototypeIdentification)
|
|
-- Pre 2.0 this was the only method of encoding used, anything else is from a newer version
|
|
if type(entry) ~= "string" then
|
|
return nil
|
|
end
|
|
return entry --[[@as string]]
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @return table<string, QuickBarSlotEncoded>?
|
|
function serialize.serialize_quick_bar(player)
|
|
local serialized
|
|
local width = v2_1_quick_bar_api and player.quick_bar_width
|
|
|
|
for i = 1, 100 do
|
|
local slot
|
|
|
|
if v2_1_quick_bar_api then
|
|
local page = math.floor((i - 1) / width) + 1
|
|
local page_slot = ((i - 1) % width) + 1
|
|
slot = player.get_quick_bar_slot(page, page_slot)
|
|
else
|
|
--- @diagnostic disable-next-line: missing-parameter
|
|
slot = player.get_quick_bar_slot(i)
|
|
end
|
|
|
|
if slot ~= nil then
|
|
local entry = serialize.serialize_quick_bar_slot(slot, player)
|
|
if entry ~= nil then
|
|
serialized = serialized or {}
|
|
serialized[tostring(i)] = entry
|
|
end
|
|
end
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param serialized table<string, string | QuickBarSlotEncoded>?
|
|
function serialize.deserialize_quick_bar(player, serialized)
|
|
-- Every slot is set, and the ones not in the data are cleared, so a slot moved or cleared on the
|
|
-- previous server does not come back from this server's old copy
|
|
serialized = serialized or {}
|
|
local width = v2_1_quick_bar_api and player.quick_bar_width
|
|
for i = 1, 100 do
|
|
local entry = serialized[tostring(i)]
|
|
local slot = entry ~= nil and serialize.deserialize_quick_bar_slot(entry, player) or nil
|
|
if v2_1_quick_bar_api then
|
|
local page = math.floor((i - 1) / width) + 1
|
|
local page_slot = ((i - 1) % width) + 1
|
|
player.set_quick_bar_slot(page, page_slot, slot)
|
|
else
|
|
--- @diagnostic disable-next-line: param-type-mismatch
|
|
player.set_quick_bar_slot(i, slot)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Crafting queue is a table with the following fields:
|
|
-- crafting_queue
|
|
-- ingredients
|
|
function serialize.serialize_crafting_queue(player)
|
|
local crafting_queue = {}
|
|
|
|
-- Give player some more inventory space to avoid duplicating items
|
|
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1000
|
|
|
|
-- Save current items
|
|
local inventory = player.character.get_main_inventory()
|
|
local old_items = inventory.get_contents()
|
|
local crafting_queue_progress = player.crafting_queue_progress
|
|
|
|
-- Cancel old crafts to get the items back
|
|
while player.crafting_queue_size > 0 do
|
|
local old_queue = player.crafting_queue
|
|
|
|
-- Cancel craft
|
|
player.cancel_crafting {
|
|
index = 1,
|
|
count = 1,
|
|
}
|
|
|
|
local rightmost_right_index = 0 -- 0 indexed since it is subtractive in a 1 indexed language
|
|
local new_queue = player.crafting_queue
|
|
while
|
|
new_queue ~= nil and
|
|
new_queue[#new_queue - rightmost_right_index] ~= nil and
|
|
new_queue[#new_queue - rightmost_right_index].count >= old_queue[#old_queue - rightmost_right_index].count
|
|
do
|
|
rightmost_right_index = rightmost_right_index + 1
|
|
end
|
|
local oldItem = old_queue[#old_queue - rightmost_right_index]
|
|
local newItem = nil
|
|
if new_queue ~= nil then
|
|
newItem = new_queue[#new_queue - rightmost_right_index]
|
|
end
|
|
|
|
-- Figure out how many items to add to queue
|
|
local added = oldItem.count
|
|
if newItem ~= nil then
|
|
added = oldItem.count - newItem.count
|
|
if oldItem.recipe ~= newItem.recipe then
|
|
log("ERROR: Old item "..oldItem.recipe.." is not equal "..newItem.recipe)
|
|
end
|
|
end
|
|
|
|
-- If the last item we added was of the same type, merge them in the queue
|
|
if #crafting_queue > 0 and crafting_queue[#crafting_queue].recipe == oldItem.recipe then
|
|
crafting_queue[#crafting_queue].count = crafting_queue[#crafting_queue].count + added
|
|
else
|
|
-- If the last item was of a different type, add a new item to the queue
|
|
table.insert(crafting_queue, {
|
|
recipe = oldItem.recipe,
|
|
count = added,
|
|
})
|
|
end
|
|
-- game.print("Saved craft "..oldItem.recipe)
|
|
end
|
|
|
|
local difference = {}
|
|
if v2_storage_api then
|
|
-- Find amount of items added and remove from inventory
|
|
local new_items = inventory.get_contents()
|
|
-- Build map of old counts by item name and quality
|
|
local old_counts = {}
|
|
for _, item in ipairs(old_items) do
|
|
local key = item.name .. ":" .. item.quality
|
|
old_counts[key] = (old_counts[key] or 0) + item.count
|
|
end
|
|
|
|
-- Compare with new items to find differences
|
|
for _, item in ipairs(new_items) do
|
|
local key = item.name .. ":" .. item.quality
|
|
local old_count = old_counts[key] or 0
|
|
local diff = item.count - old_count
|
|
|
|
if diff > 0 then
|
|
-- We don't have to worry about quality because quality can't be handcrafted
|
|
local ingredient = {
|
|
name = item.name,
|
|
count = diff
|
|
}
|
|
inventory.remove(ingredient)
|
|
table.insert(difference, ingredient)
|
|
end
|
|
end
|
|
else
|
|
-- Find amount of items added and remove from inventory
|
|
local new_items = inventory.get_contents()
|
|
for k,v in pairs(new_items) do
|
|
local old_count = old_items[k]
|
|
local diff = v
|
|
if old_count ~= nil then
|
|
diff = diff - old_count
|
|
end
|
|
if diff > 0 then
|
|
local ingredient = {
|
|
name = k,
|
|
count = diff,
|
|
}
|
|
inventory.remove(ingredient)
|
|
table.insert(difference, ingredient)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Remove extra inventory slots
|
|
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1000
|
|
|
|
local serialized = {
|
|
crafting_queue = crafting_queue,
|
|
crafting_queue_progress = crafting_queue_progress,
|
|
ingredients = difference,
|
|
}
|
|
|
|
-- Restore the crafting queue that was just destructively serialized
|
|
serialize.deserialize_crafting_queue(player, serialized)
|
|
|
|
return serialized
|
|
end
|
|
|
|
function serialize.deserialize_crafting_queue(player, serialized)
|
|
local inventory = player.character.get_main_inventory()
|
|
|
|
-- Give player some more inventory space to avoid duplicating items
|
|
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1000
|
|
|
|
-- Add items to inventory
|
|
for _, item in pairs(serialized.ingredients) do
|
|
inventory.insert(item)
|
|
end
|
|
|
|
-- Load crafting queue
|
|
for _, queueItem in pairs(serialized.crafting_queue) do
|
|
-- Start crafting (consume items)
|
|
player.begin_crafting {
|
|
count = queueItem.count,
|
|
recipe = queueItem.recipe,
|
|
-- silent = true, -- Fail silently if items are missing
|
|
}
|
|
end
|
|
|
|
-- Remove extra inventory slots
|
|
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1000
|
|
-- Set progress of current craft
|
|
player.crafting_queue_progress = serialized.crafting_queue_progress
|
|
end
|
|
|
|
---@param player LuaPlayer
|
|
---@param failed string[]?
|
|
---@return string?
|
|
function serialize.serialize_crafting_notifications(player, failed)
|
|
-- Get all unlocked recipes
|
|
local recipes = {}
|
|
for _, recipe in pairs(player.force.recipes) do
|
|
if recipe.enabled and not recipe.hidden then
|
|
recipes[recipe.name] = true
|
|
end
|
|
end
|
|
|
|
-- Remove those with notifications
|
|
for _, recipe in pairs(player.get_recipe_notifications()) do
|
|
recipes[recipe.name] = nil
|
|
end
|
|
|
|
-- Convert to a list of names, including previously failed recipes if any
|
|
local recipe_names = {}
|
|
for _, name in ipairs(failed or {}) do
|
|
recipe_names[#recipe_names + 1] = name
|
|
end
|
|
for name in pairs(recipes) do
|
|
recipe_names[#recipe_names + 1] = name
|
|
end
|
|
|
|
return helpers.encode_string(helpers.table_to_json(recipe_names))
|
|
end
|
|
|
|
--- @class CraftingNotificationDelta
|
|
--- @field add string[]? Recipes cleared elsewhere that still have a notification here
|
|
--- @field remove string[]? Recipes with a notification elsewhere that are cleared here
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param serialized string Encoded delta against the state sent with the download request
|
|
--- @return string[]? failed Recipe names that do not exist
|
|
function serialize.deserialize_crafting_notifications(player, serialized)
|
|
--- @type CraftingNotificationDelta
|
|
local delta = helpers.json_to_table(assert(helpers.decode_string(serialized)))
|
|
assert(type(delta) == "table", "wrong type decoded from json_to_table")
|
|
|
|
-- Recipes which can have a notification on this server
|
|
local enabled = {}
|
|
for _, recipe in pairs(player.force.recipes) do
|
|
if recipe.enabled and not recipe.hidden then
|
|
enabled[recipe.name] = true
|
|
end
|
|
end
|
|
|
|
-- Clear notifications the player has cleared elsewhere, this means "add seen notification"
|
|
local failed = {}
|
|
for _, recipe_name in pairs(delta.add or {}) do
|
|
if enabled[recipe_name] then
|
|
player.clear_recipe_notification(recipe_name)
|
|
else
|
|
failed[#failed + 1] = recipe_name -- Recipe does not exist on this server
|
|
end
|
|
end
|
|
|
|
-- Add notifications the player has elsewhere, this means "remove seen notification"
|
|
for _, recipe_name in pairs(delta.remove or {}) do
|
|
if enabled[recipe_name] then
|
|
player.add_recipe_notification(recipe_name)
|
|
end
|
|
end
|
|
|
|
return next(failed) and failed or nil
|
|
end
|
|
|
|
local controller_to_name = {}
|
|
for name, value in pairs(defines.controllers) do
|
|
controller_to_name[value] = name
|
|
end
|
|
|
|
--- @class SerializedPlayerData
|
|
--- @field generation number
|
|
--- @field name string
|
|
--- @field controller string?
|
|
--- @field color Color?
|
|
--- @field chat_color Color?
|
|
--- @field tag string?
|
|
--- @field force string?
|
|
--- @field cheat_mode boolean?
|
|
--- @field flashlight boolean?
|
|
--- @field shortcuts table<string, boolean>?
|
|
--- @field game_view_settings table<string, boolean>?
|
|
--- @field ticks_to_respawn number?
|
|
--- @field components SyncComponents Components this was serialized with
|
|
--- @field character table<string, any>?
|
|
--- @field inventories table<string, table>?
|
|
--- @field hotbar table<string, string>?
|
|
--- @field quick_bar table<string, QuickBarSlotEncoded>?
|
|
--- @field personal_logistic_slots table?
|
|
--- @field crafting_queue table?
|
|
--- @field recipe_notifications string?
|
|
|
|
--- @param player LuaPlayer
|
|
--- @return table<string, boolean>
|
|
function serialize.serialize_shortcuts(player)
|
|
local shortcuts = {}
|
|
for name, prototype in pairs(compat.prototypes.shortcut) do
|
|
if prototype.toggleable then
|
|
shortcuts[name] = player.is_shortcut_toggled(name)
|
|
end
|
|
end
|
|
return shortcuts
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param serialized table<string, boolean>
|
|
function serialize.deserialize_shortcuts(player, serialized)
|
|
local shortcut_prototypes = compat.prototypes.shortcut
|
|
for name, toggled in pairs(serialized) do
|
|
local prototype = shortcut_prototypes[name]
|
|
if prototype and prototype.toggleable then
|
|
player.set_shortcut_toggled(name, toggled)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @return table<string, boolean>
|
|
function serialize.serialize_game_view_settings(player)
|
|
local settings = player.game_view_settings
|
|
local serialized = {}
|
|
for _, key in pairs(game_view_settings_keys) do
|
|
serialized[key] = settings[key]
|
|
end
|
|
return serialized
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param serialized table<string, boolean>
|
|
function serialize.deserialize_game_view_settings(player, serialized)
|
|
local settings = player.game_view_settings
|
|
for _, key in pairs(game_view_settings_keys) do
|
|
if serialized[key] ~= nil then
|
|
settings[key] = serialized[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Components which are synced, nil when all of them are
|
|
--- @alias SyncComponents table<string, boolean>?
|
|
|
|
--- @param components SyncComponents
|
|
--- @param name string
|
|
--- @return boolean
|
|
local function syncs(components, name)
|
|
return components == nil or components[name] == true
|
|
end
|
|
serialize.syncs = syncs
|
|
|
|
local component_names = {
|
|
"controller", "force", "appearance", "inventories", "logistics", "quick_bar", "settings", "recipe_notifications",
|
|
}
|
|
|
|
--- True if a component synced in new_components was not synced in old_components
|
|
--- @param old_components SyncComponents
|
|
--- @param new_components SyncComponents
|
|
--- @return boolean
|
|
function serialize.newly_synced(old_components, new_components)
|
|
for _, name in ipairs(component_names) do
|
|
if syncs(new_components, name) and not syncs(old_components, name) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @return boolean
|
|
local function waiting_to_respawn(player)
|
|
return player.controller_type == defines.controllers.ghost and player.ticks_to_respawn ~= nil
|
|
end
|
|
|
|
--- Call fn with a player waiting to respawn temporarily respawned
|
|
---
|
|
--- For the waiting to respawn state the inventory logistic requests and filters are hidden on the player, and are
|
|
--- only accessible in the character controller.
|
|
--- @param player LuaPlayer
|
|
--- @param fn fun()
|
|
local function with_respawned(player, fn)
|
|
local ticks_to_respawn = player.ticks_to_respawn
|
|
player.ticks_to_respawn = nil -- Respawn now
|
|
|
|
fn()
|
|
|
|
-- Go back to waiting for respawn
|
|
local character = player.character
|
|
player.ticks_to_respawn = ticks_to_respawn
|
|
if character and character.valid then
|
|
character.destroy()
|
|
end
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param failed_deserialization FailedDeserializationPlayerData
|
|
--- @param components SyncComponents
|
|
--- @return SerializedPlayerData
|
|
function serialize.serialize_player(player, failed_deserialization, components)
|
|
local sync_controller = syncs(components, "controller")
|
|
local sync_inventories = syncs(components, "inventories")
|
|
local sync_logistics = syncs(components, "logistics")
|
|
|
|
--- @type SerializedPlayerData
|
|
local serialized = {
|
|
generation = 0, -- Gets replaced later
|
|
name = player.name,
|
|
components = components,
|
|
}
|
|
|
|
if sync_controller then
|
|
serialized.controller = controller_to_name[player.controller_type]
|
|
serialized.cheat_mode = player.cheat_mode
|
|
serialized.ticks_to_respawn = player.ticks_to_respawn
|
|
|
|
-- In 2.0 we want to sync the physical controller to ignore remote view
|
|
if v2_remote_controller then
|
|
serialized.controller = controller_to_name[player.physical_controller_type]
|
|
end
|
|
end
|
|
|
|
if syncs(components, "appearance") then
|
|
serialized.color = player.color
|
|
serialized.chat_color = player.chat_color
|
|
serialized.tag = player.tag
|
|
end
|
|
|
|
if syncs(components, "force") then
|
|
serialized.force = player.force.name
|
|
end
|
|
|
|
if syncs(components, "settings") then
|
|
serialized.flashlight = player.is_flashlight_enabled()
|
|
serialized.shortcuts = serialize.serialize_shortcuts(player)
|
|
serialized.game_view_settings = serialize.serialize_game_view_settings(player)
|
|
end
|
|
|
|
if (sync_inventories or sync_logistics) and waiting_to_respawn(player) then
|
|
with_respawned(player, function()
|
|
if sync_logistics then
|
|
serialized.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
|
end
|
|
if sync_inventories then
|
|
serialized.inventories = serialize.serialize_inventories(player, character_inventories)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Serialize character
|
|
if player.character then
|
|
if sync_inventories then
|
|
serialized.character = serialize.serialize_character(player.character)
|
|
end
|
|
if sync_logistics then
|
|
serialized.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
|
end
|
|
end
|
|
|
|
-- Serialize non-character inventories
|
|
if sync_inventories and (
|
|
player.controller_type == defines.controllers.god
|
|
or v2_remote_controller and player.physical_controller_type == defines.controllers.god
|
|
) then
|
|
serialized.inventories = serialize.serialize_inventories(player, { main = defines.inventory.god_main })
|
|
end
|
|
|
|
-- Serialize quick bar
|
|
if syncs(components, "quick_bar") then
|
|
serialized.quick_bar = serialize.serialize_quick_bar(player)
|
|
end
|
|
|
|
-- Serialize crafting queue
|
|
if sync_inventories and player.character then
|
|
serialized.crafting_queue = serialize.serialize_crafting_queue(player)
|
|
end
|
|
|
|
-- Serialize recipe notifications
|
|
if recipe_notifications_api and syncs(components, "recipe_notifications") then
|
|
serialized.recipe_notifications = serialize.serialize_crafting_notifications(player, failed_deserialization.recipe_notifications)
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
--- The controller a player has on this instance, restored in place of the synced one when it is not synced
|
|
---
|
|
--- Logistic requests and the flashlight are kept on the character, which sync code may replace.
|
|
--- @class LocalControllerState
|
|
--- @field controller string?
|
|
--- @field ticks_to_respawn number?
|
|
--- @field personal_logistic_slots table?
|
|
--- @field flashlight boolean?
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param components SyncComponents
|
|
--- @return LocalControllerState
|
|
function serialize.serialize_local_controller(player, components)
|
|
local controller_type = v2_remote_controller and player.physical_controller_type or player.controller_type
|
|
--- @type LocalControllerState
|
|
local state = {
|
|
controller = controller_to_name[controller_type],
|
|
ticks_to_respawn = player.ticks_to_respawn,
|
|
}
|
|
if player.character then
|
|
state.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
|
state.flashlight = player.is_flashlight_enabled()
|
|
elseif not syncs(components, "logistics") and waiting_to_respawn(player) then
|
|
with_respawned(player, function()
|
|
state.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
|
end)
|
|
end
|
|
return state
|
|
end
|
|
|
|
--- Find a surface characters can exist on, used when the player is on a space platform surface
|
|
--- @param platform LuaSpacePlatform
|
|
--- @return LuaSurface
|
|
local function find_planet_surface(platform)
|
|
local location = platform.space_location
|
|
local planet = location and game.planets[location.name]
|
|
if planet and planet.surface then
|
|
return planet.surface
|
|
end
|
|
for _, surface in pairs(game.surfaces) do
|
|
if not surface.platform then
|
|
return surface
|
|
end
|
|
end
|
|
error("No surface found which can hold a character")
|
|
end
|
|
|
|
--- Ensure a player has a character, works from any controller type
|
|
--- @param player LuaPlayer
|
|
--- @return LuaEntity
|
|
local function ensure_character(player)
|
|
-- Do nothing if the player has a valid character
|
|
local character = player.character
|
|
if character and character.valid then
|
|
return character
|
|
end
|
|
|
|
-- Exit remote view before switching controllers, this can fail if the player is on a platform
|
|
if v2_exit_remote_view and player.controller_type == defines.controllers.remote then
|
|
player.exit_remote_view()
|
|
end
|
|
|
|
-- Switch to god controller if create_character would fail
|
|
if player.controller_type ~= defines.controllers.god then
|
|
player.set_controller{ type = defines.controllers.god }
|
|
end
|
|
|
|
-- Characters can not be created on platform surfaces, restore_position will move them back into the hub
|
|
local surface = player.surface
|
|
if v2_space_platform and surface.platform then
|
|
local planet_surface = find_planet_surface(surface.platform)
|
|
local force = player.force --[[@as LuaForce]]
|
|
player.teleport(force.get_spawn_position(planet_surface), planet_surface)
|
|
end
|
|
|
|
-- Create and return the character
|
|
if not player.create_character() then
|
|
error(string.format(
|
|
"Failed to create character (controller: %s, physical: %s, surface: %s, connected: %s, driving: %s)",
|
|
controller_to_name[player.controller_type],
|
|
v2_remote_controller and controller_to_name[player.physical_controller_type] or "n/a",
|
|
player.surface.name, tostring(player.connected), tostring(player.driving)
|
|
))
|
|
end
|
|
return assert(player.character)
|
|
end
|
|
|
|
--- @class FailedDeserializationPlayerData
|
|
--- @field recipe_notifications string[]?
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param serialized SerializedPlayerData
|
|
--- @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
|
|
function serialize.deserialize_player(player, serialized, components, local_controller)
|
|
local failed_deserialization = {}
|
|
local sync_controller = syncs(components, "controller")
|
|
local sync_inventories = syncs(components, "inventories")
|
|
local sync_logistics = syncs(components, "logistics")
|
|
|
|
-- Stored data from instances which do not sync a component lacks its fields
|
|
local restore_controller = sync_controller and serialized.controller ~= nil
|
|
|
|
--- @type LocalControllerState?
|
|
local state = local_controller
|
|
if restore_controller then
|
|
state = { controller = serialized.controller, ticks_to_respawn = serialized.ticks_to_respawn }
|
|
end
|
|
|
|
-- A player in remote view keeps their physical controller
|
|
local current_controller = v2_remote_controller and player.physical_controller_type or player.controller_type
|
|
local target_controller = state and defines.controllers[state.controller]
|
|
local to_ghost = state and state.controller == "ghost"
|
|
local change_controller = state and (
|
|
current_controller ~= target_controller
|
|
or to_ghost and (restore_controller or sync_inventories or sync_logistics)
|
|
)
|
|
|
|
-- Items can come from a character, a god or a ghost and go to whatever controller the player ends up with
|
|
local items = serialized
|
|
local place = sync_inventories
|
|
if change_controller and not sync_inventories then
|
|
-- The local items are kept, but they have to move to the new controller
|
|
items = serialize.serialize_player(player, {}, { inventories = true })
|
|
place = true
|
|
end
|
|
local inventories = items.character and items.character.inventories or items.inventories
|
|
local queue = items.crafting_queue
|
|
local spill
|
|
|
|
local logistic_slots = serialized.personal_logistic_slots
|
|
if not sync_logistics then
|
|
logistic_slots = local_controller and local_controller.personal_logistic_slots
|
|
end
|
|
local flashlight = serialized.flashlight
|
|
if not syncs(components, "settings") then
|
|
flashlight = local_controller and local_controller.flashlight
|
|
end
|
|
|
|
if state and change_controller then
|
|
if state.controller == "character" then
|
|
-- Create a character but do not destroy an existing one
|
|
ensure_character(player)
|
|
if player.controller_type ~= target_controller then
|
|
player.set_controller{ type = target_controller }
|
|
end
|
|
|
|
elseif state.controller == "ghost" then
|
|
-- Ghost state stores hidden logistic and filters which are only accessible in the character controller
|
|
-- The force first, so the character and the logistic groups are on the restored player's force
|
|
if syncs(components, "force") and serialized.force then
|
|
player.force = serialized.force
|
|
end
|
|
local character = ensure_character(player)
|
|
serialize.deserialize_personal_logistic_slots(player, logistic_slots)
|
|
if place 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
|
|
else
|
|
-- We have to set ticks to respawn to save the hidden state into the player but we
|
|
-- can't unset tick_to_respawn by setting it back to nil as that triggers a respawn.
|
|
player.ticks_to_respawn = 0
|
|
player.set_controller{ type = defines.controllers.god }
|
|
player.set_controller{ type = defines.controllers.ghost }
|
|
end
|
|
if character and character.valid then
|
|
character.destroy()
|
|
end
|
|
|
|
elseif state.controller ~= "remote" then
|
|
-- All other controllers should not have a character
|
|
local character = player.character
|
|
if character and character.valid then
|
|
character.destroy()
|
|
end
|
|
if player.controller_type ~= target_controller then
|
|
player.set_controller{ type = target_controller }
|
|
end
|
|
|
|
else
|
|
error("Remote should not be 'serialized.controller'")
|
|
end
|
|
end
|
|
|
|
if syncs(components, "appearance") and serialized.color then
|
|
player.color = serialized.color
|
|
player.chat_color = serialized.chat_color
|
|
player.tag = serialized.tag
|
|
end
|
|
if syncs(components, "force") and serialized.force then
|
|
player.force = serialized.force
|
|
end
|
|
if restore_controller and serialized.cheat_mode ~= nil then
|
|
player.cheat_mode = serialized.cheat_mode
|
|
end
|
|
if flashlight == true then
|
|
player.enable_flashlight()
|
|
elseif flashlight == false then
|
|
player.disable_flashlight()
|
|
end
|
|
if syncs(components, "settings") then
|
|
if serialized.shortcuts then
|
|
serialize.deserialize_shortcuts(player, serialized.shortcuts)
|
|
end
|
|
if serialized.game_view_settings then
|
|
serialize.deserialize_game_view_settings(player, serialized.game_view_settings)
|
|
end
|
|
end
|
|
|
|
-- Deserialize items, a ghost got them while it had a character above
|
|
local character = player.character
|
|
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
|
|
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
|
|
if character then
|
|
serialize.deserialize_personal_logistic_slots(player, logistic_slots)
|
|
end
|
|
|
|
-- Deserialize quick bar (named hotbar in old data)
|
|
if syncs(components, "quick_bar") then
|
|
serialize.deserialize_quick_bar(player, serialized.quick_bar or serialized.hotbar)
|
|
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, spill
|
|
end
|
|
|
|
return serialize
|