Keep the local controller intact when it is not synced

With the controller off, a god or ghost player was still turned into a spectator while downloading, which threw away the local state the sync is meant to leave alone. Only spectate a player without a character when the controller is synced, and skip the ghost round trip when the ghost is the local state.

A ghost on an instance that syncs inventories but not the controller never received the stored character, so after respawning it uploaded an empty one over it. Respawn such a ghost straight away when there is a stored character to give it.

Clear the saved local controller when the player picks a temporary inventory after a failed download, otherwise a later download restores the stale controller over the temporary character. An empty download only gives a character when the controller is synced or the player was spectated.

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 0e559cf000
commit c798b2a33f
5 files changed
+12 -3

No files matched your search

+1
View File
@@ -104,6 +104,7 @@ Defaults to `true`.
Sync the character and god inventories, the character bonuses and the crafting queue.
While the controller is not synced only the inventory of a character is synced, a player without a character keeps the inventory they have on the instance and does not change the stored one.
A player waiting to respawn is respawned straight away if there is a synced character to give them.
Defaults to `true`.
@@ -34,7 +34,7 @@ local function download_inventory(player_name, data, number, total)
progress_dialog.remove(player)
-- Give the player a character and pretend that's the synced player data
if serialize.syncs(script_data.components, "inventories") then
if serialize.syncs(script_data.components, "controller") or player_record.local_controller then
ensure_character(player)
end
player_record.local_controller = nil
@@ -131,6 +131,7 @@ dialog_failed_download.events = {
player.gui.screen.dialog_failed_download.destroy()
player_sync.players[player.name].dirty = true
player_sync.players[player.name].sync = false
player_sync.players[player.name].local_controller = nil
elseif event.element.name == "player_sync_failed_download_retry" then
-- Retry acquiring the player from the controller
+2 -1
View File
@@ -487,7 +487,8 @@ 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 and syncs("inventories") then
-- Without the controller synced a player without a character has nothing synced to protect
if player_record.sync and syncs("inventories") and (syncs("controller") or player.character) then
-- Store original position to teleport back to
if v2_remote_controller then
record.surface = player.physical_surface
+7 -1
View File
@@ -987,10 +987,16 @@ function serialize.deserialize_player(player, serialized, components, local_cont
local state = local_controller
if restore_controller then
state = { controller = serialized.controller, ticks_to_respawn = serialized.ticks_to_respawn }
elseif state and state.controller == "ghost" and sync_inventories and serialized.character then
-- Respawn now, a ghost would drop the synced character and upload an empty one after respawning
state = { controller = "character" }
end
local target_controller = state and defines.controllers[state.controller]
if state and (player.controller_type ~= target_controller or state.controller == "ghost") then
if state and (
player.controller_type ~= target_controller
or restore_controller and state.controller == "ghost"
) then
if state.controller == "character" then
-- Create a character but do not destroy an existing one
ensure_character(player)