Skip to content

Player identity across a disconnect and rejoin

A player's Human.ReferenceId is stable across a disconnect and rejoin into the same running server world. The Human is never destroyed on disconnect and the rejoining client re-possesses the same object. Four specific cases produce a new id.

Line numbers in this page refer to the 0.2.6403.27689 decompile of Assembly-CSharp.dll.

How ReferenceId is assigned

Referencable (37867) holds the allocator. Ids are sequential, server-authoritative, and persisted in the save. Not random, not a hash, not derived from the Steam id.

public static class Referencable
{
    public static long NextReferenceId = 1L;

    public static object NextReferenceIdLock = new object();

    public static readonly List<IReferencable> ReferencablesChanged = new List<IReferencable>();

    public static readonly Dictionary<long, IReferencable> Referencables = new Dictionary<long, IReferencable>();

    public static List<IReferencable> FailedToRegister = new List<IReferencable>();

    public const long INVALID = 0L;

RegisterNew (37900) is the fresh-spawn path. It throws if called on a client (37902-37910):

    public static bool RegisterNew(IReferencable iReferencable)
    {
        if (Assets.Scripts.Networking.NetworkManager.IsClient)
        {
            string text = "Error: Clients should not be assigning a new Reference";
            if (iReferencable != null)
            {
                text = text + ". Client is trying to assign a new reference for " + iReferencable.DisplayName;
            }
            throw new System.Exception(text);
        }

and then hands out the next number under the lock (37921-37930):

        lock (NextReferenceIdLock)
        {
            if (Referencables.ContainsKey(NextReferenceId))
            {
                ConsoleWindow.PrintError($"error trying to register '{iReferencable.DisplayName}' with referenceId of '{NextReferenceId}' as it is already Assigned to: '{Referencables[NextReferenceId].DisplayName}'");
                return false;
            }
            iReferencable.ReferenceId = NextReferenceId;
            NextReferenceId++;
        }

RegisterAs (37940) is the adopt-an-existing-id path used by save load and by client join:

    public static bool RegisterAs(IReferencable thing, long referenceId, bool force = false)
            lock (NextReferenceIdLock)
            {
                thing.ReferenceId = referenceId;
                referenceId++;
                NextReferenceId = ((referenceId > NextReferenceId) ? referenceId : NextReferenceId);
            }

Load re-adopts the saved id: Thing.Create<Thing>(prefab, pos, rot, thingData.ReferenceId) reaches the referenceId != 0 branch inside Thing.Create<T> (319024-319027):

            else
            {
                flag = Referencable.RegisterAs(thing2, referenceId);
            }

The referenceId parameter of Thing.Create<T> is the discriminator throughout: 0L means "server-side fresh spawn, allocate via RegisterNew", nonzero means "recreate a known thing under an existing id via RegisterAs". See StructureRegistration for the full Thing.Create<T> body.

Disconnect leaves the Human in the world

NetworkServer.ClientDisconnected (213891) is the entire server-side teardown:

        public static void ClientDisconnected(long connectionId)
        {
            Client client = Client.Find(connectionId);
            if (client != null)
            {
                client.SetState(ClientState.Disconnected);
                ConsoleWindow.Print("Client disconnected: " + client.ToStringOneLine());
                NetworkBase.RemoveClient(client);
            }
        }

and NetworkBase.RemoveClient (39227) is:

    public static void RemoveClient(Client client)
    {
        Clients.Remove(client);
        OnClientRemoved();
    }

No Thing destruction, no Referencable deregistration, no Brain.PlayerBrains removal. The body stays in the world as an offline entity.

Brain.IsOnline (340989) is defined purely as connection presence:

        public bool IsOnline => Client.Find(ClientId) != null;

Offline bodies keep metabolising at a reduced rate. Brain.OnLifeTick (341225-341238) gates the rate on DifficultySetting.Current.OfflineMetabolism:

        public override void OnLifeTick()
        {
            base.OnLifeTick();
            if (!ParentEntity)
            {
                return;
            }
            bool flag = ParentEntity is Npc;
            float num = ((flag || IsOnline) ? 0.2f : (0.2f * Mathf.Clamp(DifficultySetting.Current.OfflineMetabolism, 0.1f, 1f)));
            if (!flag && ((object)ParentHuman == null || ParentHuman.IsSleeping))
            {
                num *= 0.5f;
            }
            float num2 = ((flag || IsOnline) ? 3f : (3f * Mathf.Clamp(DifficultySetting.Current.OfflineMetabolism, 0.1f, 1f)));

The only code that unbinds a brain from a client id is OnServer.RelinquishBrain (40470) and Brain.RelinquishControl (341127):

    public static void RelinquishBrain(Brain playerBrain)
    {
        Brain.PlayerBrains.Remove(playerBrain.ClientId);
        playerBrain.ClientControl = false;
        playerBrain.ClientId = 0uL;
    }
        public void RelinquishControl()
        {
            LocalControl = false;
            ParentEntity?.ReleaseControl();
            PlayerBrains.Remove(ClientId);
            if (Assets.Scripts.Networking.NetworkManager.IsClient)
            {
                NetworkClient.ReturnCharacter();
            }
            InventoryManager.ParentBrain = null;
            InventoryManager.Parent = null;
            ClientId = 0uL;
        }

Those are reached only from the respawn paths Human.Respawn() (361921) and Human.RespawnFromNoParent() (361956), and from RelinquishControlMessage (278569). Nothing on the disconnect path touches them.

Rejoin re-possesses the same object

NetworkClient.ProcessJoinData (213188) ends with a character request (213229-213230):

                await ImGuiLoadingScreen.SetState(GameStrings.LoadingScreenRequestingCharacter.DisplayString);
                await RequestCharacterAsync(isRespawn: false, cancellationToken);

RequestCharacterAsync (213286) is the decision point. A new Human is requested only when there is no surviving brain for this Steam id, or when the caller explicitly asks for a respawn:

        public static async UniTask RequestCharacterAsync(bool isRespawn, CancellationToken token)
        {
            ConsoleWindow.Print("[NetworkClient] Sending respawn message and waiting to take control");
            ulong localClientId = Assets.Scripts.Networking.NetworkManager.LocalClientId;
            PlayerCosmetics cosmetics = PlayerCosmetics.Load(Singleton<GameManager>.Instance.CustomCosmeticsSlot);
            Brain.GetValidatedBrain(localClientId, out var playerBrain);
            if ((object)playerBrain == null || isRespawn)
            {
                SendToServer(new RespawnMessage(localClientId, cosmetics));
            }
            await TakeControl(token);
        }

Brain.GetValidatedBrain (341304) is a Steam-id lookup into PlayerBrains, with a decay check:

        public static void GetValidatedBrain(ulong steamId, out Brain playerBrain)
        {
            PlayerBrains.TryGetValue(steamId, out playerBrain);
            if ((object)playerBrain != null && (object)playerBrain.ParentHuman != null && playerBrain.ParentHuman.State == EntityState.Decay)
            {
                playerBrain.ClientId = 0uL;
                if (GameManager.RunSimulation)
                {
                    OnServer.Destroy(playerBrain);
                }
                playerBrain = null;
            }
        }

PlayerBrains (340944) is a plain static dictionary keyed by Steam id:

        public static readonly Dictionary<ulong, Brain> PlayerBrains = new Dictionary<ulong, Brain>();

It is rebuilt on the joining client by Brain.DeserializeOnJoin (341216), which ends in RegisterBrain(ClientId):

        public override void DeserializeOnJoin(RocketBinaryReader reader)
        {
            base.DeserializeOnJoin(reader);
            ClientId = reader.ReadUInt64();
            SteamName = reader.ReadString();
            ClientControl = reader.ReadBoolean();
            RegisterBrain(ClientId);
        }
        public void RegisterBrain(ulong steamId)
        {
            ClientId = steamId;
            PlayerBrains[steamId] = this;
        }

TakeControl (213308) then possesses the existing body and echoes its id back (213321, 213327-213329):

                    playerBrain.TakeControl(setPhysics: false);
                    takeControlMessage.HumanId = playerBrain.ParentHuman.ReferenceId;
                    takeControlMessage.ClientId = localClientId;
                    takeControlMessage.SendToServer();

No Human.CreateCharacter, no OnServer.Create<Human>, no RegisterNew. Same Human, same ReferenceId.

The four cases that do produce a new ReferenceId

Case Trigger Mechanism Result
Body fully decayed while offline ParentHuman.State == EntityState.Decay at rejoin GetValidatedBrain (341304) nulls ClientId, destroys the brain, returns null, so RequestCharacterAsync takes the RespawnMessage branch New Human via Human.CreateCharacter (362098) and OnServer.Create<Human> (362115) with referenceId == 0, which reaches RegisterNew and a new sequential id
Player died and chose respawn Human.Respawn() (361921) or Human.RespawnFromNoParent() (361956) Both call NetworkClient.RequestCharacterAsync(isRespawn: true, ...) (361935 / 361964), forcing the RespawnMessage branch regardless of the brain lookup New Human, new id. See RespawnFlow
Character relinquished RelinquishControlMessage (278569) OnServer.RelinquishBrain (40470) does Brain.PlayerBrains.Remove(playerBrain.ClientId) and playerBrain.ClientId = 0uL The next join finds no brain for that Steam id, so GetValidatedBrain yields null and a new Human is created
A different Steam account Any join under a different Steam id PlayerBrains is keyed by Steam id No entry, so a new Human

The creation site (362098, 362115):

        public static Human CreateCharacter(ulong clientId, string steamName, PlayerCosmetics cosmetics = null, bool isRespawn = false, StartLocationData startLocation = null, ISpawnPoint spawnPoint = null)
            Human human = OnServer.Create<Human>(Prefab.Character, safePositionInRadius, rotation);

CreateCharacter does reuse spawn continuity, but that is start-location data, not the ReferenceId. RespawnMessage.Process (278542-278546) looks up the previous client info to decide the spawn point:

            SerializedClientInfo clientInfo = GameManager.GetClientInfo(HumanId);
            bool isRespawn = clientInfo != null;
            StartLocationData startLocation = ((clientInfo != null) ? DataCollection.Get<StartLocationData>(clientInfo.StartLocationHash) : null);
            ISpawnPoint spawnPoint = ((clientInfo != null) ? Referencable.Find<ISpawnPoint>(clientInfo.SpawnPointReference) : null);
            Human.CreateCharacter(HumanId, client?.name ?? "Unknown", Cosmetics, isRespawn, startLocation, spawnPoint);

and CreateCharacter writes the record back at 362145:

            GameManager.AddClientInfo(clientId, new SerializedClientInfo(clientId, startLocation.IdHash, spawnPoint?.ReferenceId ?? 0));

ReferenceId remapping on load

Normal load preserves ids exactly. Remapping happens only on collision or failure. RegisterAs pushes onto FailedToRegister in three cases (37949-37966): the id is 0, the thing already holds an id, or the id is already taken.

            if (referenceId == 0L)
            {
                ConsoleWindow.PrintError("error trying to assign " + thing.DisplayName + " a null reference id");
                FailedToRegister.Add(thing);
                return false;
            }
            if (!force && thing.ReferenceId != 0L)
            {
                ConsoleWindow.PrintError($"error trying to register '{thing.DisplayName}' with id '{referenceId}' as it already has been registered with '{thing.ReferenceId}' ");
                FailedToRegister.Add(thing);
                return false;
            }
            if (Referencables.ContainsKey(referenceId))
            {
                ConsoleWindow.PrintError($"Couldn't register '{thing.DisplayName}' with id '{referenceId}' as there is already an entry for this id. It is used by: {Referencables[referenceId].DisplayName}");
                FailedToRegister.Add(thing);
                return false;
            }

World load then calls Referencable.AssignNewIdToDuplicates() (268759-268761):

            SpawnDataHelper.LoadSave(worldData);
            Referencable.AssignNewIdToDuplicates();
            await UniTask.NextFrame();
    public static void AssignNewIdToDuplicates()
    {
        foreach (IReferencable item in FailedToRegister)
        {
            RegisterNew(item);
            ConsoleWindow.PrintAction(item.DisplayName + "' has been assigned a new Id: " + StringManager.Get(item.ReferenceId));
        }
        FailedToRegister.Clear();
    }

Referencable.FindAndSetNextReferenceId(worldData) (38060) scans the saved collections and parks the allocator past the highest saved id (38091-38094):

    public static void FindAndSetNextReferenceId(XmlSaveLoad.WorldData worldData)
        lock (NextReferenceIdLock)
        {
            NextReferenceId = highest + 1;
        }

Referencable.ClearReferences() (38043) resets the allocator to 1:

    public static void ClearReferences()
    {
        lock (Referencables)
        {
            Referencables.Clear();
        }
        lock (NextReferenceIdLock)
        {
            NextReferenceId = 1L;
        }
    }

It is reached via Thing.ClearAll() in World.Initialize (325012-325019), which is why ids restart at 1 for a fresh world:

    private static void Initialize(string worldName, bool newWorld, string loadingScreenMessage)
    {
        if (!GameManager.IsBatchMode)
        {
            XmlSaveLoad.UpdateLoadingScreen(loadingScreenMessage, 0f).Forget();
        }
        Thing.ClearAll();
        GameManager.GameState = GameState.Joining;

The three deployment scenarios

Dedicated server that keeps running: stable. The world is never torn down, Referencables is never cleared, the Human is never destroyed, and PlayerBrains[steamId] survives the disconnect. A player who drops and rejoins gets the same Human with the same ReferenceId.

Host / listen server: stable for remote clients. A remote client disconnecting and rejoining behaves exactly like the dedicated case. The host leaving shuts the world down, which is the next case.

Save reloaded from the menu: still stable for the same Steam account. Ids are re-adopted by RegisterAs, and brain ownership round-trips through the save. BrainSaveData (340934-340941) carries the Steam id:

public class BrainSaveData : DynamicThingSaveData
{
    [XmlElement]
    public ulong ClientSteamId;

    [XmlElement]
    public PlayerCosmetics identity;
}

Brain.DeserializeSave (341153) restores the mapping:

        public override void DeserializeSave(ThingSaveData savedData)
        {
            base.DeserializeSave(savedData);
            if (savedData is BrainSaveData brainSaveData)
            {
                ClientId = brainSaveData.ClientSteamId;
            }
            if (ClientId != 0)
            {
                PlayerBrains[ClientId] = this;
            }
        }

World.HandlePlayerControl() (324973) then matches the local Steam id against PlayerBrains and takes control of the existing body:

    public static DynamicThing HandlePlayerControl()
    {
        foreach (KeyValuePair<ulong, Brain> playerBrain in Brain.PlayerBrains)
        {
            if (playerBrain.Key != Assets.Scripts.Networking.NetworkManager.LocalClientId)
            {
                continue;
            }
            Brain value = playerBrain.Value;
            if ((bool)value)
            {
                if ((bool)(value.ParentSlot.Parent as Entity))
                {
                    value.TakeControl();
                }
                else
                {
                    value.TakeControlInBodyBag();
                }
                return value.ParentSlot.Parent.AsDynamicThing;
            }
        }
        return null;
    }

Only if that returns null does the loader fall through to World.CreateCharacterAndTakeControl() and a new Human. The call site (268783-268791):

            else if ((bool)World.HandlePlayerControl())
            {
                await LodManager.InitialiseLodsOnLoad();
            }
            else
            {
                LodManager.EnqueueRequesterToUpdate(World.CreateCharacterAndTakeControl());
                await LodManager.InitialiseLodsOnLoad();
            }

and the definition (324998):

    public static Human CreateCharacterAndTakeControl()
    {
        ulong localClientId = Assets.Scripts.Networking.NetworkManager.LocalClientId;
        string username = Assets.Scripts.Networking.NetworkManager.Username;
        PlayerCosmetics cosmetics = PlayerCosmetics.Load(Singleton<GameManager>.Instance.CustomCosmeticsSlot) ?? new PlayerCosmetics();
        SerializedClientInfo value;
        bool flag = GameManager.ClientInfo.TryGetValue(localClientId, out value);
        StartLocationData startLocation = (flag ? DataCollection.Get<StartLocationData>(value.StartLocationHash) : null);
        ISpawnPoint spawnPoint = (flag ? Referencable.Find<ISpawnPoint>(value.SpawnPointReference) : null);
        Human human = Human.CreateCharacter(localClientId, username, cosmetics, flag, startLocation, spawnPoint);
        human.OrganBrain.TakeControl(setPhysics: false);
        return human;
    }

The caveat on this third scenario is AssignNewIdToDuplicates: a save carrying duplicate or zero ids can have arbitrary Things renumbered on load. That is a corrupted-save path, not normal operation.

When the server's process restarts under a remote player

The three scenarios above follow a remote player through a server that keeps running, and a save's own local player through a reload. None follows a remote player through a restart of the server's process: a dedicated server restarting into its save, or a listen host relaunching the game and loading its world again, while the remote player stays in their own game and joins the new process. Line numbers in this subsection refer to the 0.2.6428.27798 decompile, where every member cited is textually identical in the client and the dedicated server builds.

The id comes back when the save holds the player's character. The save carries the player's brain with its Steam id (BrainSaveData.ClientSteamId, 346885). The restarted server's Brain.DeserializeSave (347101-347112) puts that brain back into Brain.PlayerBrains, the join payload hands it to the rejoining client, whose Brain.DeserializeOnJoin (347164-347171) registers it the same way (RegisterBrain, 347283-347287), and NetworkClient.RequestCharacterAsync (218933-218944) then finds it through GetValidatedBrain and takes control of the saved body rather than sending a RespawnMessage: the mechanism of "Rejoin re-possesses the same object" and of the third scenario. Measured on the test rig on 2026-09-27, where the rig's reload saves the world under its own name before it stops the server and loads it again: a joiner's Human read 509126 before and after, and 509106 before and after in each of the nine earlier dedicated-server restarts of that day whose records were kept. A listen host's relaunch keeps it the same way, measured on 2026-09-28 in two runs in which the joiner went to its menu, the host relaunched into its save and the joiner joined again: its Human read 509317 before and after, and 509313 before and after. Dedicated-server restarts that day kept 509119, 509123, and 509126 twice in one run.

When the save does not hold it, the player gets a new Human. Measured on the rig on 2026-09-25: a dedicated server that was stopped without saving and then loaded the same save again came back without the character the joiner had been given in the unsaved session, and the rejoin gave the player a new Human, 509159 in place of 510297. The new id is lower because the loaded world hands out ids again from past the highest id in the save (Referencable.FindAndSetNextReferenceId, 38063-38099, sets NextReferenceId = highest + 1; see "ReferenceId remapping on load"). What survives is the id the save carries, not the id the player had a moment before.

What does not survive is anything the server kept in memory. The new process starts with every static empty, so server-side mod state keyed by the Human.ReferenceId is gone although the key comes back. The client's process did not restart. It left its world through GameManager.LeaveGame: a RakNet ConnectionLost (279323-279326) reaches NetworkManager.PlayerDisconnected, which on NetworkRole.Client calls GameManager.LeaveGame() (279371-279382), and LeaveGame tears the world down at the end of that frame (see GameManager, "Leaving a session: LeaveGame returns before the world is gone"). Its own statics carry into the next join, where the same key is back. Measured the same day, on the joiner's console, in order: A client lost the connection., Client Disconnecting from Host, Game State: None, Ending network connection, Resetting network state, a second Ending network connection, then Game State: Joining and a new Connection ID as it joined again in the same game process. After it the joiner still held Spray Paint Plus's preference mask row 32760 under Human 509126, as both processes had before the restart, and through 15 s of reads that began 3 s after a spray gun was put in the joiner's hand, the restarted dedicated server held no row for it at all.

Why this matters for mod state

Mod state keyed by Human.ReferenceId on a server survives a player's reconnect. That cuts both ways.

  • Per-player rows do not self-expire when a player leaves. They must be pruned explicitly, which is what a Prefix on NetworkServer.ClientDisconnected is for. See ClientDisconnectedPrefix.
  • A client-side cache keyed by the local Human's ReferenceId is still valid after a rejoin. Any "resend only when the value changed" dedupe built on it stays silent across the reconnect, because the key and the last-sent value both still match. If a rejoin must force a resend, clear the cache on the join path rather than relying on the id to change.

A server restart breaks the symmetry a rejoin keeps

Both bullets above assume the server's process keeps running, so the server keeps its copy while the client keeps its own. Across a restart of that process ("When the server's process restarts under a remote player", above) the id comes back and the server's copy does not. A "resend only when the value changed" dedupe on the client then stays silent exactly when the server needs the value again: the client compares against its own record, which lived through the restart in its own process, finds nothing new and sends nothing, and the server holds nothing for that player. Spray Paint Plus shipped exactly this through v1.12.1 with its per-player preference mask. Its server reads an absent row as allowing everything, so after a server restart that gave a returning player their Human back, that player's Paint Single Item By Default and client-side opt-outs were ignored until they pressed Shift or Ctrl (see SprayPaintPlusNetworking, "A restarted server gets the mask back").

Ways to force the resend, and the obvious one that does not work on its own:

  • Key the client's record to the controlled body, not to its id. A session builds its world afresh on the client: leaving destroys every Thing (GameManager.ClearGameAll calls Thing.ClearAll(), 210288, and Thing.ClearAll, 328171, destroys each one), and a join or a load creates each Thing again (a load starts with Thing.ClearAll() in World.Initialize, 330953). So the local Human is a new object after every join, load and respawn, even when its ReferenceId is the old one. A record that remembers which object it last reported for, held through a WeakReference so a world that was left is not kept alive, owes a report on the first frame of every session and needs no hook. Not in a release yet: Spray Paint Plus does this in PreferenceMaskReports, which is in commit fdcffdc5 on master, and is targeted at v1.13.0. In v1.12.1, after a server restart, the server reads a rejoining player as allowing everything until they press Shift or Ctrl. Measured on the test rig on 2026-09-28 by the check a joiner's own paint preferences still apply after the server restarts, across a listen host's relaunch and a dedicated server's restart: on a build without it (Release DLL sha256 56763957) the restarted server still held no mask for the returning player after 30 s with a can in that player's hand, and on fdcffdc5's build (c0c00c18) it held the player's mask by the first read with a can in hand (see SprayPaintPlusNetworking, "A restarted server gets the mask back").
  • Clear the record on the join path, once the new world exists, as the second bullet above says. By then ClearGameAll has run and the old world is gone.
  • Not on GameManager.LeaveGame alone. LeaveGame is on every way a client leaves a session while its process keeps running, but it returns with the old world still standing: GameManager.ClearGameAll tears it down at the end of that frame, and InventoryManager.NormalMode can still run in between with the old Human in hand, depending on the order of GameManager.Managers, which is scene data and not in the code. A record cleared by a LeaveGame postfix can be spent in that window on a send into a closing connection, and the next session then sends nothing. See GameManager, "Leaving a session: LeaveGame returns before the world is gone".

Verification history

  • 2026-07-27: page created against the 0.2.6403.27689 decompile of Assembly-CSharp.dll. All excerpts read directly from the decompile and confirmed at the cited line numbers by an independent pass.
  • 2026-07-27: while writing this page, the then-current skeleton and mechanism sentence on ClientDisconnectedPrefix were found to contradict the verified signature NetworkServer.ClientDisconnected(long connectionId). A fresh validator was spawned per Research/WORKFLOW.md Rule 3 and returned "B is correct": the method takes a single long connectionId, NetworkBase.RemoveClient only removes the instance from the static Clients list and mutates nothing on the Client itself, and a Harmony patch parameter named client fails to bind at patch time. A parallel pass in the same session reached the identical verdict and rewrote that page; its own Verification History carries the full resolution. Nothing on this page was changed by the conflict, and the disconnect excerpts here match the ones on the rewritten page.
  • 2026-09-27 (game version 0.2.6428.27798): added two subsections, each with its own stamp; the rest of the page and its stamps are untouched, and its 0.2.6403.27689 line numbers stand. "When the server's process restarts under a remote player", under the deployment scenarios: the id comes back when the save holds the player's character (the brain chain re-read at 0.2.6428.27798, Brain.DeserializeSave, DeserializeOnJoin, RegisterBrain, GetValidatedBrain, NetworkClient.RequestCharacterAsync, each textually identical in the client and dedicated server decompiles; measured on the rig on 2026-09-27, Human 509126 kept across a restart in the dedicated-server cell of 'removing the last glow leaves no side-car', lock owner d718eb1f, and 509106 kept in nine earlier restarts that day); a new, lower id when the save does not hold it (measured 2026-09-25, 510297 became 509159 after a restart that had not saved; FindAndSetNextReferenceId re-read); and the client's side of the restart, a RakNet ConnectionLost reaching GameManager.LeaveGame (ReceiveEvents 279319-279326, PlayerDisconnected 279371-279382), with the joiner's console and the preference-mask rows read on both processes after the rejoin. "A server restart breaks the symmetry a rejoin keeps", under "Why this matters for mod state": the dedupe that stays silent when the server lost its copy, Spray Paint Plus's shipped instance of it, and three ways to force a resend, one of them Spray Paint Plus's fix in commit b9fce88d (unreleased, marked). Drafted in .work/2026-09-27-phase3-rig/M5-RESULT.md section 7.3 (a); changed while checking it: the earlier restarts that kept 509106 are nine with records, not three; the 510297 case was a restart that had not saved, which gave the player a new Human rather than a body the save held; and the console sequence includes the second Ending network connection that NetworkClient.Disconnect prints 500 ms after the first. Additive: every earlier statement on the page is about a server whose process keeps running, and the two bullets of "Why this matters for mod state" stand as written, so no fresh validator was needed. The top-level verified_in moved to 0.2.6428.27798 for these two subsections.
  • 2026-09-28: the Spray Paint Plus fix this page marks landed on master as fdcffdc5; the cited files are unchanged from the branch commits named in the entries above, so only the markers and sources were updated to cite it. Still not in a release (v1.12.1 is the published build).
  • 2026-09-28 (game version 0.2.6428.27798): added rig readings to both 0.2.6428.27798 subsections, from the runs of a joiner's own paint preferences still apply after the server restarts (cells JLH and JDS, on a build without fdcffdc5, Release DLL sha256 56763957, and on fdcffdc5's build, c0c00c18) and of removing the last glow leaves no side-car on c0c00c18. "When the server's process restarts under a remote player": a listen host's relaunch into its save kept the joiner's Human (509317, and 509313 in the second run), which the subsection had in scope but had not measured, and three more dedicated-server restarts kept theirs. "A server restart breaks the symmetry a rejoin keeps": the Spray Paint Plus fix in the first bullet, which the entry of 2026-09-27 recorded as source only, measured on both builds. Read from each cell's reloads.txt and restart.txt. Additive; no earlier statement changes. Both subsections restamped; the marker stays, since the fix is not in a release.

Open questions

  • Whether an in-world confirmation run matches the source reading has not been done. The whole page is source-derived; a dedicated-server session that records a player's Human.ReferenceId, disconnects, rejoins, and re-reads it would close the loop. (A rejoin across a restart of the server's process has since been measured, as of 2026-09-27: the id came back when the save held the character; see "When the server's process restarts under a remote player". A rejoin into a server that kept running has still not been measured.)