From 1c5210c2101ca15c63830e4b405c67d7169720da Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:12:30 +0200 Subject: [PATCH] Handle create_character failures in inventory sync (#975) ensure_character asserted on player.create_character() returning false without any context (#912). The most likely trigger is the player being physically inside a space platform hub (or otherwise stuck in remote view) on the destination server: the character in the hub is destroyed while downloading and the god controller ends up on the platform surface where a character can not be created. - Explicitly exit remote view and move the god controller off platform surfaces before create_character; restore_position re-enters the platform - Include controller/surface/connection state in the error message - On deserialization failure treat the player as a temporary inventory (sync = false) so the incomplete state is not uploaded over synced data - Release the acquisition on leave when nothing is uploaded so the player is not left "busy" on other instances Co-authored-by: Claude Fable 5 --- .../inventory_sync/module/inventory_sync.lua | 13 ++++-- plugins/inventory_sync/module/serialize.lua | 40 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index ca452d4c..bf56a57e 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -314,15 +314,20 @@ function inventory_sync.sync_player(acquire_response) end function inventory_sync.finish_download(player, finished_record) + local script_data = get_script_data() + script_data.finished_downloads[player.name] = nil + local player_record = script_data.players[player.name] + local status, result = xpcall(inventory_sync.deserialize_player, debug.traceback, player, finished_record) if not status then log("ERROR: Deserializing player " .. player.name .. " failed: " .. result) player.print("ERROR: Deserializing player data failed: " .. result) + -- Treat this as a temporary inventory so the incomplete state is not uploaded over the synced data + player_record.dirty = true + player_record.sync = false + return end - local script_data = get_script_data() - script_data.finished_downloads[player.name] = nil - local player_record = script_data.players[player.name] player_record.dirty = true player_record.sync = true player_record.generation = finished_record.generation @@ -380,6 +385,8 @@ inventory_sync.events[defines.events.on_pre_player_left_game] = function(event) end if not player_record.dirty or not player_record.sync then + -- Nothing to upload, release the acquisition so other instances are not blocked + clusterio_api.send_json("inventory_sync_release", { player_name = player.name }) return end diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index 93e11343..a46f6472 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -7,6 +7,8 @@ local serialize = {} local v2_logistic_api = compat.version_ge("2.0.0") local v2_storage_api = compat.version_ge("2.0.0") local v2_remote_controller = compat.version_ge("2.0.0") +local v2_space_platform = compat.version_ge("2.0.0") +local v2_exit_remote_view = compat.version_ge("2.0.56") local recipe_notifications_api = compat.version_ge("2.0.67") local v2_0_quick_bar_api = compat.version_ge("2.0.0") local v2_1_quick_bar_api = compat.version_ge("2.1.0") @@ -666,6 +668,23 @@ function serialize.serialize_player(player, failed_deserialization) return serialized end +--- Find a surface characters can exist on, used when the player is on a space platform surface +--- @param platform LuaSpacePlatform +--- @return LuaSurface +local function find_planet_surface(platform) + local location = platform.space_location + local planet = location and game.planets[location.name] + if planet and planet.surface then + return planet.surface + end + for _, surface in pairs(game.surfaces) do + if not surface.platform then + return surface + end + end + error("No surface found which can hold a character") +end + --- Ensure a player has a character, works from any controller type --- @param player LuaPlayer --- @return LuaEntity @@ -676,13 +695,32 @@ local function ensure_character(player) return character end + -- Exit remote view before switching controllers, this can fail if the player is on a platform + if v2_exit_remote_view and player.controller_type == defines.controllers.remote then + player.exit_remote_view() + end + -- Switch to god controller if create_character would fail if player.controller_type ~= defines.controllers.god then player.set_controller{ type = defines.controllers.god } end + -- Characters can not be created on platform surfaces, restore_position will move them back into the hub + local surface = player.surface + if v2_space_platform and surface.platform then + local planet_surface = find_planet_surface(surface.platform) + player.teleport(player.force.get_spawn_position(planet_surface), planet_surface) + end + -- Create and return the character - assert(player.create_character(), "Failed to create character") + if not player.create_character() then + error(string.format( + "Failed to create character (controller: %s, physical: %s, surface: %s, connected: %s, driving: %s)", + controller_to_name[player.controller_type], + v2_remote_controller and controller_to_name[player.physical_controller_type] or "n/a", + player.surface.name, tostring(player.connected), tostring(player.driving) + )) + end return player.character end