Allow inventory sync components to be turned off per instance

Adds an inventory_sync.sync_<component> instance config field for the
controller, force, appearance, inventories, logistics, quick bar,
settings and recipe notifications. All default to on, which keeps the
current behaviour.

A component which is off on an instance is left alone there and its
fields are not stored from that instance. The controller merges each
upload with the stored data using the uploading instance's config, the
instance strips the fields from downloads before sending them over RCON,
and the scenario is told the enabled set on start and when it changes so
it can skip serializing them and skip the steps which replace the
character.

The controller is only synced together with the inventories, as switching
controller can destroy the character holding them. Without the controller
synced the player gets back the controller they had on the instance, only
character inventories move between instances, and a player without a
character keeps their inventory.

Closes #968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-17 15:25:06 +00:00
1 parent e810020db5
commit 0e559cf000
11 files changed
+571 -81

No files matched your search

+31 -8
View File
@@ -24,6 +24,18 @@ local function is_in_cutscene(player)
return player.controller_type == defines.controllers.cutscene
end
-- Returns true if the component is synced on this instance
local function syncs(name)
return serialize.syncs(get_script_data().components, name)
end
-- Remember the controller the player had before sync code replaced it, restored when the controller is not synced
local function save_local_controller(player, player_record)
if not player_record.local_controller then
player_record.local_controller = serialize.serialize_local_controller(player)
end
end
-- Create player record for bookkeeping
local function create_player(player, dirty)
local script_data = get_script_data(true) -- no early return to support loading from single player
@@ -51,6 +63,11 @@ player_sync = {}
-- This function is called by instance.js as /sc player_sync.download_inventory("danielv", Escaped JSON string, package_number, total_packages_count) with data from the controller
player_sync.download_inventory = download_inventory
-- This function is called by instance.js with the components synced on this instance
function player_sync.set_components(data)
get_script_data().components = assert(compat.json_to_table(data))
end
-- This function is called internally when a player leaves the game to
-- serialize the player for upload.
function player_sync.serialize_player(player, player_record)
@@ -78,7 +95,7 @@ function player_sync.serialize_player(player, player_record)
end
local failed_deserialization = get_script_data().failed_deserialization[player.name] or {}
local serialized_player = serialize.serialize_player(player, failed_deserialization)
local serialized_player = serialize.serialize_player(player, failed_deserialization, get_script_data().components)
serialized_player.generation = player_record.generation
return serialized_player
@@ -95,11 +112,13 @@ function player_sync.deserialize_player(player, finished_record)
return
end
-- Stash temporary inventory if it exists
local stashed_corpse
local script_data = get_script_data()
local player_record = script_data.players[player.name]
if player_record.dirty and player.character then
local local_controller = player_record.local_controller or serialize.serialize_local_controller(player)
-- Stash temporary inventory if it exists, without inventory sync the current inventory is the one to keep
local stashed_corpse
if syncs("inventories") and player_record.dirty and player.character then
local character = player.character
local surface = character.surface
local position = character.position
@@ -112,7 +131,9 @@ function player_sync.deserialize_player(player, finished_record)
-- Deserialize downloaded player data
local serialized_player = compat.json_to_table(finished_record.data)
assert(type(serialized_player) == "table", "wrong type for serialized_player")
script_data.failed_deserialization[player.name] = serialize.deserialize_player(player, serialized_player)
script_data.failed_deserialization[player.name] =
serialize.deserialize_player(player, serialized_player, script_data.components, local_controller)
player_record.local_controller = nil
-- Restore player position and driving state
restore_position(player, finished_record)
@@ -291,8 +312,9 @@ function player_sync.sync_player(acquire_response)
local player_record = script_data.players[acquire_response.player_name]
if acquire_response.status ~= "acquired" then
if player_record.sync and not player_record.dirty then
if player_record.sync and not player_record.dirty and syncs("inventories") then
if player.controller_type == defines.controllers.character then
save_local_controller(player, player_record)
local character = player.character
player.set_controller({ type = defines.controllers.spectator })
if character then character.destroy() end
@@ -454,7 +476,7 @@ function player_sync.initiate_inventory_download(player, player_record, generati
-- The plugin only sends back what differs from the current notification state
local recipe_notifications
if recipe_notifications_api then
if recipe_notifications_api and syncs("recipe_notifications") then
recipe_notifications = serialize.serialize_crafting_notifications(player)
end
@@ -465,7 +487,7 @@ function player_sync.initiate_inventory_download(player, player_record, generati
-- If this is a synced player turn them into a spectator while the
-- player data is downloading
if player_record.sync then
if player_record.sync and syncs("inventories") then
-- Store original position to teleport back to
if v2_remote_controller then
record.surface = player.physical_surface
@@ -478,6 +500,7 @@ function player_sync.initiate_inventory_download(player, player_record, generati
record.vehicle = player.vehicle
end
save_local_controller(player, player_record)
local character = player.character
player.set_controller({ type = defines.controllers.spectator })
if character ~= nil then character.destroy() end