From 18afae46865dc19317ae669a4f4cbf611c543169 Mon Sep 17 00:00:00 2001 From: ILLISIS Date: Sat, 17 Oct 2026 01:46:30 +1100 Subject: [PATCH] Add set_destination remote interface to player_sync A plugin that moves a player calls remote.call("player_sync", "set_destination", player_name, instance_id) before connect_to_server. It only records the destination. Every leave clears it, and a leave that uploads with reason switching_servers adds it to the upload as destination_instance_id. The README describes it for plugin authors. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/player_sync/README.md | 19 ++++++++++++++++ plugins/player_sync/module/player_sync.lua | 26 ++++++++++++++++++++++ plugins/player_sync/module/serialize.lua | 1 + 3 files changed, 46 insertions(+) diff --git a/plugins/player_sync/README.md b/plugins/player_sync/README.md index 850f1724..5f7c16c3 100644 --- a/plugins/player_sync/README.md +++ b/plugins/player_sync/README.md @@ -91,6 +91,25 @@ Communication between the instance and the controller goes over websockets. Overall, we are able to achieve a latency between 3 and infinite ticks from server join, depending on the size of the inventory. The major limiter is rcon transfer speeds with larger inventories, especially if they contain blueprints. +## Sending player data ahead + +A plugin that moves a player to another instance with `LuaPlayer.connect_to_server` can tell player sync where the player is going, just before the call: + +```lua +local ps = remote.interfaces.player_sync +if ps and ps.set_destination then + pcall(remote.call, "player_sync", "set_destination", player.name, instance_id) -- a failure never stops the connect +end +player.connect_to_server({ address = address, name = name }) +``` + +`instance_id` is the Clusterio instance id of the destination. The call only records it, and a later call replaces it. +When the player then leaves with the reason `switching_servers`, the upload carries the destination and the controller sends the stored data on to that instance while the client reconnects. +On joining, a copy that the acquire reply confirms is applied without a download. +The destination's `player_sync.sync_*` options apply to the copy as to a download. +A cancelled dialog, a quit or a crash sends nothing. +If the destination isn't running, has player sync off or is the player's current instance, the join downloads as before. + ## Controller Configuration ### player_sync.player_lock_timeout diff --git a/plugins/player_sync/module/player_sync.lua b/plugins/player_sync/module/player_sync.lua index 7e7718d9..bb9248da 100644 --- a/plugins/player_sync/module/player_sync.lua +++ b/plugins/player_sync/module/player_sync.lua @@ -83,6 +83,24 @@ function player_sync.set_components(data) get_script_data().components = assert(compat.json_to_table(data)) end +-- For plugins that move players to another instance: remote.call("player_sync", "set_destination", player_name, +-- instance_id) just before LuaPlayer.connect_to_server, with the destination's Clusterio instance id. It only records +-- it; if the player then leaves to switch servers, the upload carries it and the controller sends the player's data +-- on to that instance (README, "Sending player data ahead"). A later call replaces it, and every leave clears it. +remote.add_interface("player_sync", { + set_destination = function(player_name, instance_id) + -- Integers only: table_to_json writes inf and nan bare, which the host can't parse, so the upload carrying it + -- would fail on every retry and the session would be lost (inf % 1 and nan % 1 are nan, never 0) + if type(instance_id) ~= "number" or instance_id % 1 ~= 0 then + return + end + local player_record = get_script_data().players[player_name] + if player_record then + player_record.destination = instance_id + end + 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) @@ -486,6 +504,9 @@ player_sync.events[defines.events.on_pre_player_left_game] = function(event) local script_data = get_script_data() local player = assert(game.get_player(event.player_index)) local player_record = script_data.players[player.name] + -- Recorded by set_destination. Every leave clears it; only an uploading switching_servers leave sends it on. + local destination = player_record.destination + player_record.destination = nil -- If player has an active download release our acquisition but let the download continue if script_data.active_downloads[player.name] then @@ -506,6 +527,11 @@ player_sync.events[defines.events.on_pre_player_left_game] = function(event) player.print("ERROR: Serializing player data failed: " .. result) return end + -- The controller sends the stored data on to that instance while the client reconnects. In the serialized table, + -- so a retried upload (check_active_uploads) carries it too. + if destination and event.reason == defines.disconnect_reason.switching_servers then + result.destination_instance_id = destination + end script_data.active_uploads[player.name] = { serialized = result, last_attempt = game.ticks_played, diff --git a/plugins/player_sync/module/serialize.lua b/plugins/player_sync/module/serialize.lua index 8659bb1d..41d37f4f 100644 --- a/plugins/player_sync/module/serialize.lua +++ b/plugins/player_sync/module/serialize.lua @@ -959,6 +959,7 @@ end --- @field personal_logistic_slots table? --- @field crafting_queue table? --- @field recipe_notifications string? +--- @field destination_instance_id integer? Only on a leave upload, for instance.ts, which strips it --- @param player LuaPlayer --- @return table