mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Complete rework of the inventory synchronization to reduce the number of times the inventory has to be downloaded and the possibilities for item loss/duplication. The basic concept is to acquire exclusive access to the player data before downloading the data. This prevents the player from logging in to two servers at the same time and getting the synced inventory twice. The acquisition will in most cases execute faster than the player can provide input to the game on login, and if the player data on the master isn't what was previously uploaded then the player inventory is deleted and the player is turned into a spectator while the updated inventory is being transferred into the game. This model allows for the downloading of the synced inventory to be skipped in the case the player joins the same server they left from. The synced data is expanded to include more than just the character and inventory to properly handle sync of dead players. If the player leaves dead on one server this state will be saved and when the player joins another server they continue off as dead. The ghost, god, and spectator controllers are supported, while the cutscene and editor controllers are not. For players in a cutscene when joining the acquisition will be attempted immediately, but the scenario waits until the player is out of the cutscene before acting upon the response to the acquisition. For players in the editor mode the scenario toggles them out of the mode before syncing data. Since uploads to the master can fail handling of this is also added. The scenario will retry uploading the player data at exponentially increasing intervals until it gets the message back that the upload succeeded. Since it's possible for servers to go down and stay down for a long time the exclusive access held by these servers are released after a timeout. This means that a player could be playing and or have uploaded newer data by the time the instance comes back online and tries to upload the inventory, in this case the master will report the upload succeeded but discard the data. Instances may also upload old data if they load back to an earlier save, which will also be discarded by the master. Other small improvements made by this rework: - Add a retry button to the failed download dialog - Update the existing progress dialog instead of destroying and recreating it every time. - Automatically stuff the items from the temporary inventory into the synced inventory if they fit. - Move player back to the position they started the download from if they were turned into a spectator for the download. - Ensure the player has a character if the stored data is wiped. - Add error checking of the RCON commands sent by the instance.
254 lines
8.2 KiB
Lua
254 lines
8.2 KiB
Lua
local clusterio_serialize = require("modules/clusterio/serialize")
|
|
local character_inventories = require("modules/inventory_sync/define_player_inventories")
|
|
local character_stat_keys = require("modules/inventory_sync/define_player_stat_keys")
|
|
local serialize = {}
|
|
|
|
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
|
|
-- 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)
|
|
|
|
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)
|
|
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)
|
|
local serialized = nil
|
|
|
|
-- Serialize personal logistic slots
|
|
for i = 1, 200 do -- Nobody will have more than 200 logistic slots, right?
|
|
local slot = player.get_personal_logistic_slot(i)
|
|
if slot.name ~= nil then
|
|
if not serialized then
|
|
serialized = {}
|
|
end
|
|
serialized[tostring(i)] = {
|
|
name = slot.name,
|
|
min = slot.min,
|
|
max = slot.max,
|
|
}
|
|
end
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
function serialize.deserialize_personal_logistic_slots(player, serialized)
|
|
if not serialized then
|
|
return
|
|
end
|
|
|
|
-- Load personal logistic slots
|
|
for i = 1, 200 do
|
|
local slot = serialized[tostring(i)] -- 1 is empty to force array to be spare
|
|
if slot ~= nil then
|
|
player.set_personal_logistic_slot(i, slot)
|
|
end
|
|
end
|
|
end
|
|
|
|
local controller_to_name = {}
|
|
for name, value in pairs(defines.controllers) do
|
|
controller_to_name[value] = name
|
|
end
|
|
|
|
-- Players are serialized into a table with the following fields:
|
|
-- name
|
|
-- controller: may not be cutscene or editor
|
|
-- color
|
|
-- chat_color
|
|
-- tag
|
|
-- force
|
|
-- cheat_mod
|
|
-- flashlight
|
|
-- ticks_to_respawn (optional)
|
|
-- character (optional)
|
|
-- inventories: non-character inventories
|
|
-- hotbar: table of string indexes to names (optional)
|
|
-- personal_logistic_slots (optional)
|
|
function serialize.serialize_player(player)
|
|
local serialized = {
|
|
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(),
|
|
ticks_to_respawn = player.ticks_to_respawn,
|
|
}
|
|
|
|
-- 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
|
|
character.destroy()
|
|
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 then
|
|
serialized.inventories = serialize.serialize_inventories(player, { main = defines.inventory.god_main })
|
|
end
|
|
|
|
-- Serialize hotbar
|
|
for i = 1, 100 do
|
|
local slot = player.get_quick_bar_slot(i)
|
|
if slot ~= nil and slot.name ~= nil then
|
|
if not serialized.hotbar then
|
|
serialized.hotbar = {}
|
|
end
|
|
serialized.hotbar[tostring(i)] = slot.name
|
|
end
|
|
end
|
|
|
|
return serialized
|
|
end
|
|
|
|
function serialize.deserialize_player(player, serialized)
|
|
if player.controller_type ~= defines.controllers[serialized.controller] or serialized.controller == "ghost" then
|
|
-- If targeting the character or ghost controller then create a character
|
|
if serialized.controller == "character" or serialized.controller == "ghost" then
|
|
if player.controller_type == defines.controllers.ghost or player.controller_type == defines.controllers.spectator then
|
|
player.set_controller({ type = defines.controllers.god })
|
|
end
|
|
if player.controller_type == defines.controllers.god then
|
|
player.create_character()
|
|
end
|
|
|
|
-- The ghost state stores hidden logistic and filters which are only accessible in the character controller
|
|
if serialized.controller == "ghost" then
|
|
serialize.deserialize_personal_logistic_slots(player, serialized.personal_logistic_slots)
|
|
serialize.deserialize_inventories(player, serialized.inventories, character_inventories)
|
|
local character = player.character
|
|
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.controller.god })
|
|
player.set_controller({ type = defines.controller.ghost })
|
|
end
|
|
character.destroy()
|
|
end
|
|
|
|
else
|
|
-- Targeting the god or spectator controller, if coming from the
|
|
-- character controller then destroy the character
|
|
if player.controller_type == defines.controllers.character then
|
|
player.character.destroy()
|
|
end
|
|
|
|
-- Switching to god or spectator is a matter of setting the controller
|
|
if player.controller_type ~= defines.controllers[serialized.controller] then
|
|
player.set_controller({ type = defines.controllers[serialized.controller] })
|
|
end
|
|
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
|
|
|
|
-- 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 hotbar
|
|
if serialized.hotbar then
|
|
for i = 1, 100 do
|
|
if serialized.hotbar[tostring(i)] ~= nil then
|
|
player.set_quick_bar_slot(i, serialized.hotbar[tostring(i)])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return serialize
|