Skip to content

Skip SaveAndReload in HostServer when hosting from replay#4

Closed
sviyh wants to merge 1 commit intodevfrom
fix/skip-saveandreload-on-replay-host
Closed

Skip SaveAndReload in HostServer when hosting from replay#4
sviyh wants to merge 1 commit intodevfrom
fix/skip-saveandreload-on-replay-host

Conversation

@sviyh
Copy link
Copy Markdown
Owner

@sviyh sviyh commented Apr 23, 2026

Summary

  • HostUtil.CreateGameData always calls SaveLoad.SaveAndReload. The reload step is needed for the SP-host path (canonicalizes MP-injected state) but is wasted work for the replay-host path (state was just deserialized from disk).
  • The wasted reload introduces a Verse.Rand stream perturbation: mod-side spawn-time graphic randomization (e.g. AdaptiveStorage Framework's ThingGrid.RegisterInCell transpiler hook) consumes Rand inside SeedMapFinalizeLoading's seeded scope at different stream positions on the host's in-process reload vs the client's fresh-process load. Result: divergent RoomTempTracker.equalizeCells orderings → temperature drift → desync at the next FreezeManager.DoIceMelting Rand.Chance boundary (~120 ticks later).
  • Fix: pass fromReplay to CreateGameData. Use SaveLoad.SaveGameData (save without reload) on the replay path. SP-host path unchanged.

Investigation

Probe instrumentation captured the divergence:

  • Host load count per Map.FinalizeLoading event:
    • tick 9954061 (replay file load via Root_Play.Start): 42 regens, byte-identical to client
    • tick 9954062 (SaveAndReload via HostUtil.CreateGameData): 84 regens, divergent shuffle output
    • tick 9956175 (SaveAndReload via CreateJoinPointAndSendIfHost): also asymmetric, but fires after the desync is already manifest
  • WallEqProbe captured runtime divergence at tick 9954127 — host's equalizeCells for room r=45 came from the SaveAndReload's shuffle output, client's from the fresh-process load.
  • RandSeedProbe captured the offending Rand consumer: AdaptiveStorage's Graphic_Random.get_MatSingle chain via the ThingGrid.RegisterInCell transpiler hook on every item spawn during Map.FinalizeLoading.

Test plan

  • Build clean Release DLL
  • Deploy to Workshop path (D:\Steam\steamapps\workshop\content\294100\2606448745\1.6\AssembliesCustom\)
  • Restore stock MP-Compat to isolate the MP fix in test
  • Replay-host repro: load the same MP save that previously desynced at tick 9954128, host it, let client join. Expect: no desync past the prior divergence point.
  • SP-host regression: load a SP save, host MP, let client join. Expect: state stays canonical (SaveAndReload still runs in this path).
  • Single-player smoke test: confirm no SP behaviour change (the gate is on fromReplay which only matters when hosting MP).

Notes

  • Does not address Load Seed RoomTempTracker.RegenerateEqualizeCells with per-room deterministic state #3 (auto-generated join-point on client connect). That fires after the host has been alive for some time, and SaveAndReload there serves a different purpose (capture transient in-memory state for client snapshot). Left untouched.
  • Fixes the immediate desync class for replay-host without requiring per-mod compat patches.

HostUtil.CreateGameData unconditionally called SaveLoad.SaveAndReload to
canonicalize the host's in-memory state via a serialize+deserialize roundtrip
before sending the snapshot to clients. This is necessary for the SP-host path
because SetupGameFromSingleplayer injects MP-specific state that needs to
roundtrip through the load path to reach a canonical form.

When hosting from a replay (fromReplay=true), the state was just deserialized
from disk by Root_Play.Start -> LoadGameFromSaveFileNow and is already canonical.
The roundtrip is wasted work, and worse, the in-process reload introduces
Verse.Rand stream perturbations that don't occur on the client's fresh-process
load. Mod-side spawn-time graphic randomization (e.g. AdaptiveStorage Framework
via the ThingGrid.RegisterInCell transpiler hook) consumes Verse.Rand inside
SeedMapFinalizeLoading's seeded scope at different stream positions on the host
vs the client, shifting subsequent RegenerateEqualizeCells.Shuffle outputs and
producing divergent equalizeCells per room. Within ~120 ticks the temperature-
equalization drift propagates into a FreezeManager.DoIceMelting Rand.Chance
short-circuit boundary and the main RNG stream desyncs.

Switch CreateGameData to take fromReplay and use SaveGameData (save without
reload) for the replay path. SP-host path is unchanged.
@sviyh sviyh changed the base branch from master to dev April 23, 2026 05:18
@sviyh sviyh closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant