Keep a syncing spectator where its body stood

A player waiting for player_sync data is a spectator who could walk away,
so restore_position teleported them back at the end. That also undid
teleports by other mods made during the sync, such as universal_edges
placing an arriving player.

The spectator is now pulled back on every tile change, and a jump of more
than 4 tiles or to another surface counts as another mod's teleport and
moves where the body comes back. A failed acquire that takes the body
keeps its place too.

Closes #908

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-11-03 07:34:06 +00:00
1 parent cfea463c7b
commit babc210a38
2 files changed
+57 -10

No files matched your search

+3
View File
@@ -77,6 +77,9 @@ The greatly simplified data flow is as follows:
7. Scenario displays and updates a progress bar as each chunk is received.
8. Once all chunks have loaded the player's synced inventory is recreated from the data and the player can start playing.
While the player is a spectator waiting for the data they can't walk away, and their character comes back where it stood.
A mod that teleports the spectator more than 4 tiles, or to another surface, moves where the character comes back to.
When the player leaves the inventory is uploaded if it's a synced inventory and the exclusive access the scenario script holds is released.
Should an error occur during this process the player is given the option to use a temporary inventory instead, which will be merged back into the synced inventory the next time the sync succeeds on that instance.
A download that stops part way, for example because the controller went down, is started over the next time the player joins, and nothing is uploaded for the player until it finishes.
+54 -10
View File
@@ -73,6 +73,19 @@ local function remove_inventory_bonus_raise(script_data, player_name)
end
end
-- Where the player's body stands, in the shape restore_position takes
local function body_place(player)
local place = { vehicle = player.driving and player.vehicle or nil }
if v2_remote_controller then
place.surface = player.physical_surface
place.position = player.physical_position
else
place.surface = player.surface
place.position = player.position
end
return place
end
-- Turn a synced player into a spectator while their player data is downloaded or applied, if this instance syncs
-- inventories, and keep where their body stood in record for restore_position. The old body goes, with its crafting
-- queue. Until the data is applied, the player's state isn't persisted.
@@ -93,16 +106,10 @@ local function spectate(player, player_record, record)
-- Store original position to teleport back to. Only once per record: a rejoin while a prefetched copy is held
-- spectates again, and the spectator's position then isn't where the body stood
if record.position == nil then
if v2_remote_controller then
record.surface = player.physical_surface
record.position = player.physical_position
else
record.surface = player.surface
record.position = player.position
end
if player.driving then
record.vehicle = player.vehicle
end
local here = body_place(player)
record.surface = here.surface
record.position = here.position
record.vehicle = here.vehicle
end
save_local_controller(player, player_record, record.components)
@@ -125,6 +132,18 @@ local function keep_body_place(player_record, record)
end
end
-- Where the body goes back when sync code gives it back, if it took it
local function body_place_of(script_data, player_name)
for _, downloads in ipairs({ script_data.active_downloads, script_data.finished_downloads }) do
local record = downloads[player_name]
if record and record.position ~= nil then
return record
end
end
local player_record = script_data.players[player_name]
return player_record and player_record.body_place
end
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
@@ -487,6 +506,7 @@ function player_sync.sync_player(acquire_response)
and player.controller_type == defines.controllers.character
then
save_local_controller(player, player_record, player_record.components)
player_record.body_place = body_place(player)
local character = player.character
player.set_controller({ type = defines.controllers.spectator })
if character then character.destroy() end
@@ -567,6 +587,30 @@ player_sync.events[defines.events.on_player_joined_game] = function(event)
script_data.active_uploads[player.name] = nil
end
-- A spectator whose body sync code took stays where the body stood. A jump of more than 4 tiles, or to another surface,
-- is another mod's teleport and moves where the body goes back instead.
player_sync.events[defines.events.on_player_changed_position] = function(event)
local player = assert(game.get_player(event.player_index))
if player.controller_type ~= defines.controllers.spectator then
return
end
local place = body_place_of(get_script_data(), player.name)
if not place then
return
end
local position = player.position --[[@as MapPosition.struct]]
local dx = position.x - place.position.x
local dy = position.y - place.position.y
if player.surface == place.surface and dx * dx + dy * dy <= 16 then
player.teleport(place.position, place.surface)
else
place.surface = player.surface
place.position = position
place.vehicle = nil
end
end
-- Raised for a restored armor after its equipment's inventory bonus applied, the temporary bonus is no longer needed
player_sync.events[defines.events.on_player_armor_inventory_changed] = function(event)
local player = assert(game.get_player(event.player_index))