mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
The scenario no longer sends its components with the download request. The instance strips the download by its own config and sends the components it used with the first chunk. If they differ from the ones the scenario started the download with, because the config changed in between, the scenario restarts the download. A pending download is now marked with sync_in_progress instead of clearing dirty while spectating, and it is set for every download rather than only when inventories are synced. Uploads are skipped while it is set, so a player who leaves during a download with inventories off no longer uploads their stale state. SyncComponent names the components in the Lua annotations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1385 lines
45 KiB
Lua
1385 lines
45 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
|
|
|
|
--- @alias SyncComponent
|
|
--- | "controller"
|
|
--- | "force"
|
|
--- | "appearance"
|
|
--- | "inventories"
|
|
--- | "logistics"
|
|
--- | "quick_bar"
|
|
--- | "settings"
|
|
--- | "recipe_notifications"
|
|
|
|
--- Components which are synced, nil when all of them are
|
|
--- @alias SyncComponents table<SyncComponent, boolean>?
|
|
|
|
--- @param components SyncComponents
|
|
--- @param name SyncComponent
|
|
--- @return boolean
|
|
local function syncs(components, name)
|
|
return components == nil or components[name] == true
|
|
end
|
|
serialize.syncs = syncs
|
|
|
|
--- @type SyncComponent[]
|
|
local component_names = {
|
|
"controller", "force", "appearance", "inventories", "logistics", "quick_bar", "settings", "recipe_notifications",
|
|
}
|
|
|
|
--- True if the same components are synced in both
|
|
--- @param a SyncComponents
|
|
--- @param b SyncComponents
|
|
--- @return boolean
|
|
function serialize.same_components(a, b)
|
|
for _, name in ipairs(component_names) do
|
|
if syncs(a, name) ~= syncs(b, name) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- 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
|