mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Rename inventory_sync to player_sync
The plugin syncs more than inventories now (settings, appearance, recipe notifications), so name it for what it does. The package becomes @clusterio/plugin-player_sync and every inventory_sync identifier follows. A migration entrypoint renames the inventory_sync config fields, including load_plugin, and the inventory view permission. The plugin moves inventories.json in the database directory to player_sync.json and storage.inventory_sync in saves to storage.player_sync itself, and closes dialogs left open under the old GUI names on startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e455d17bb7
commit
8363c5f0ef
39 files changed
+331
-177
No files matched your search
@@ -0,0 +1,886 @@
|
||||
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
|
||||
|
||||
function serialize.deserialize_inventories(destination, serialized, inventories)
|
||||
for name, index in pairs(inventories) do
|
||||
local inventory = destination.get_inventory(index)
|
||||
if inventory ~= nil and serialized[name] ~= nil then
|
||||
inventory.clear()
|
||||
clusterio_serialize.deserialize_inventory(inventory, serialized[name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Characters are serialized into a table with the following fields:
|
||||
-- character_crafting_speed_modifier
|
||||
-- character_mining_speed_modifier
|
||||
-- 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
|
||||
|
||||
function serialize.deserialize_character(character, serialized)
|
||||
-- Deserialize character stats
|
||||
for _, key in pairs(character_stat_keys) do
|
||||
character[key] = serialized[key]
|
||||
end
|
||||
|
||||
-- Deserialize character inventories
|
||||
serialize.deserialize_inventories(character, serialized.inventories, character_inventories)
|
||||
|
||||
-- Deserialize armor grid state, the grid exists after the armor inventory is restored
|
||||
local grid = character.grid
|
||||
if grid and serialized.inhibit_movement_bonus ~= nil then
|
||||
grid.inhibit_movement_bonus = serialized.inhibit_movement_bonus
|
||||
end
|
||||
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
|
||||
|
||||
local serialized = nil
|
||||
|
||||
-- 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
|
||||
if not serialized then
|
||||
serialized = {}
|
||||
end
|
||||
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 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 get added with name only - this avoids overwriting existing groups on the server
|
||||
sec.group = section.group
|
||||
else
|
||||
-- Unnamed groups get added with filters
|
||||
sec.filters = section.filters
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for i, slot in pairs(serialized) do
|
||||
if slot ~= nil then
|
||||
player.set_personal_slogistic_slot(tonumber(i), slot)
|
||||
end
|
||||
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 }
|
||||
--- @alias QuickBarSlotEncoded QuickBarSlotEncoded.name | QuickBarSlotEncoded.filter
|
||||
|
||||
--- @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
|
||||
--- @return QuickBarSlotEncoded | nil
|
||||
function serialize.serialize_quick_bar_slot(slot)
|
||||
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
|
||||
|
||||
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
|
||||
--- @return QuickBarSlot | ItemWithQualityID | nil
|
||||
function serialize.deserialize_quick_bar_slot(entry)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
-- entry.t == "f" is only remaining case for 2.0
|
||||
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
|
||||
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)
|
||||
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)
|
||||
if not serialized then
|
||||
return
|
||||
end
|
||||
|
||||
local width = v2_1_quick_bar_api and player.quick_bar_width
|
||||
for i = 1, 100 do
|
||||
local entry = serialized[tostring(i)]
|
||||
if entry ~= nil then
|
||||
local slot = serialize.deserialize_quick_bar_slot(entry)
|
||||
if slot ~= nil then
|
||||
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
|
||||
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 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
|
||||
|
||||
--- @param player LuaPlayer
|
||||
--- @param failed_deserialization FailedDeserializationPlayerData
|
||||
--- @return SerializedPlayerData
|
||||
function serialize.serialize_player(player, failed_deserialization)
|
||||
--- @type SerializedPlayerData
|
||||
local serialized = {
|
||||
generation = 0, -- Gets replaced later
|
||||
controller = controller_to_name[player.controller_type],
|
||||
name = player.name,
|
||||
color = player.color,
|
||||
chat_color = player.chat_color,
|
||||
tag = player.tag,
|
||||
force = player.force.name,
|
||||
cheat_mode = player.cheat_mode,
|
||||
flashlight = player.is_flashlight_enabled(),
|
||||
shortcuts = serialize.serialize_shortcuts(player),
|
||||
game_view_settings = serialize.serialize_game_view_settings(player),
|
||||
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
|
||||
|
||||
-- For the waiting to respawn state the inventory logistic requests and filters are hidden on the player
|
||||
if player.controller_type == defines.controllers.ghost and player.ticks_to_respawn then
|
||||
player.ticks_to_respawn = nil -- Respawn now
|
||||
|
||||
serialized.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
||||
serialized.inventories = serialize.serialize_inventories(player, character_inventories)
|
||||
|
||||
-- Go back to waiting for respawn
|
||||
local character = player.character
|
||||
player.ticks_to_respawn = serialized.ticks_to_respawn
|
||||
if character and character.valid then
|
||||
character.destroy()
|
||||
end
|
||||
end
|
||||
|
||||
-- Serialize character
|
||||
if player.character then
|
||||
serialized.character = serialize.serialize_character(player.character)
|
||||
serialized.personal_logistic_slots = serialize.serialize_personal_logistic_slots(player)
|
||||
end
|
||||
|
||||
-- Serialize non-character inventories
|
||||
if 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
|
||||
serialized.quick_bar = serialize.serialize_quick_bar(player)
|
||||
|
||||
-- Serialize crafting queue
|
||||
if player.character then
|
||||
serialized.crafting_queue = serialize.serialize_crafting_queue(player)
|
||||
end
|
||||
|
||||
-- Serialize recipe notifications
|
||||
if recipe_notifications_api then
|
||||
serialized.recipe_notifications = serialize.serialize_crafting_notifications(player, failed_deserialization.recipe_notifications)
|
||||
end
|
||||
|
||||
return serialized
|
||||
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
|
||||
--- @return FailedDeserializationPlayerData?
|
||||
function serialize.deserialize_player(player, serialized)
|
||||
local failed_deserialization = {}
|
||||
|
||||
local target_controller = defines.controllers[serialized.controller]
|
||||
if player.controller_type ~= target_controller or serialized.controller == "ghost" then
|
||||
if serialized.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 serialized.controller == "ghost" then
|
||||
-- Ghost state stores hidden logistic and filters which are only accessible in the character controller
|
||||
local character = ensure_character(player)
|
||||
if serialized.personal_logistic_slots then
|
||||
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
||||
end
|
||||
if serialized.inventories then
|
||||
serialize.deserialize_inventories(player, serialized.inventories, character_inventories)
|
||||
end
|
||||
if serialized.ticks_to_respawn then
|
||||
player.ticks_to_respawn = serialized.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 serialized.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
|
||||
|
||||
player.color = serialized.color
|
||||
player.chat_color = serialized.chat_color
|
||||
player.tag = serialized.tag
|
||||
player.force = serialized.force
|
||||
player.cheat_mode = serialized.cheat_mode
|
||||
if serialized.flashlight then
|
||||
player.enable_flashlight()
|
||||
else
|
||||
player.disable_flashlight()
|
||||
end
|
||||
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
|
||||
|
||||
-- Deserialize character
|
||||
if player.character then
|
||||
serialize.deserialize_character(player.character, serialized.character)
|
||||
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
||||
end
|
||||
|
||||
-- Deserialize non-character inventories
|
||||
if player.controller_type == defines.controllers.god then
|
||||
serialize.deserialize_inventories(player, serialized.inventories, { main = defines.inventory.god_main })
|
||||
end
|
||||
|
||||
-- Deserialize quick bar (named hotbar in old data)
|
||||
serialize.deserialize_quick_bar(player, serialized.quick_bar or serialized.hotbar)
|
||||
|
||||
-- Deserialize crafting queue
|
||||
if player.character and serialized.crafting_queue then
|
||||
serialize.deserialize_crafting_queue(player, serialized.crafting_queue)
|
||||
end
|
||||
|
||||
-- Deserialize recipe notifications
|
||||
if recipe_notifications_api 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
|
||||
end
|
||||
|
||||
return serialize
|
||||
Reference in new issue
Block a user