From babc210a385895ea07d937c43d53eb75024d25c6 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:17:51 +0000 Subject: [PATCH] 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 --- plugins/player_sync/README.md | 3 + plugins/player_sync/module/player_sync.lua | 64 ++++++++++++++++++---- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/plugins/player_sync/README.md b/plugins/player_sync/README.md index 95d77498..d36fbd40 100644 --- a/plugins/player_sync/README.md +++ b/plugins/player_sync/README.md @@ -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. diff --git a/plugins/player_sync/module/player_sync.lua b/plugins/player_sync/module/player_sync.lua index 0c86bbc2..5cfa2f8c 100644 --- a/plugins/player_sync/module/player_sync.lua +++ b/plugins/player_sync/module/player_sync.lua @@ -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))