Skip to content

Adaptive Storage Framework: isolate spawn-time graphic Rand#586

Closed
sviyh wants to merge 1 commit intorwmt:masterfrom
sviyh:fix/adaptive-storage-rand-isolation
Closed

Adaptive Storage Framework: isolate spawn-time graphic Rand#586
sviyh wants to merge 1 commit intorwmt:masterfrom
sviyh:fix/adaptive-storage-rand-isolation

Conversation

@sviyh
Copy link
Copy Markdown
Contributor

@sviyh sviyh commented Apr 23, 2026

Summary

  • Adds a compat patch for adaptive.storage.framework (by bbradson) wrapping spawn-time graphic-randomization in Rand.PushState(thingIDNumber) / PopState so it can't perturb MP's SeedMapFinalizeLoading seeded scope during host's SaveAndReload.
  • LoadPatch is deferred via LongEventHandler.ExecuteWhenFinished to avoid triggering AS's ThingExtensions cctor before DefDatabase is populated.

The bug

Adaptive Storage Framework's item-graphic code reaches Verse.Graphic_Random.get_MatSingle during spawn, which consumes Verse.Rand to pick a random material variant. Two entry points trigger this chain:

  1. AdaptiveStorage.ThingClass.InitializeStoredThings — bulk re-add when a storage container itself spawns
  2. AdaptiveStorage.ThingClass.Notify_ItemRegisteredAtCell — per-item hook fired by AS's transpiler on ThingGrid.RegisterInCell for every item spawn landing in an AS storage cell

Full stack (captured from probe instrumentation on a reproducing session):

Verse.Rand.Range
  Verse.Graphic_Random.get_MatSingle
    Verse.Graphic.get_Shader
      Verse.GraphicData.GraphicColoredFor
        Verse.Thing.get_DefaultGraphic
          Verse.Thing.get_Graphic
            RimWorld.MinifiedThing.get_Graphic
              AdaptiveStorage.ItemGraphicWorker.GetGraphicFor
                AdaptiveStorage.StorageRenderer.SetPrintDataDirty
                  AdaptiveStorage.StorageRenderer.AssignThingGraphic
                    AdaptiveStorage.ThingCollection.Add
                      AdaptiveStorage.ThingClass.InitializeStoredThings
                        AdaptiveStorage.ThingClass.OnSpawn
                          → Map.FinalizeLoading → SaveLoad.LoadInMainThread → SaveAndReload

During host's in-process SaveAndReload the spawn-order of these Rand calls relative to RoomTempTracker.RegenerateEqualizeCells.Shuffle differs from the client's fresh-process load (same 8174 things spawned, different ordering). Both pull from the same SeedMapFinalizeLoading-pushed seed, but at different iteration counts, producing divergent equalizeCells per room. Within ~120 ticks the temperature drift propagates into a FreezeManager.DoIceMelting Rand.Chance short-circuit and the main RNG stream desyncs.

The fix

Wrap both entry points with Rand.PushState(__instance.thingIDNumber) / PopState (MP-only). Same pattern as MP's own SeedPawnGraphics for PawnRenderer.SetAllGraphicsDirty. The seed is stable across save/load and identical across clients, so each container picks the same graphic variant every time it spawns — no functional change.

Why LoadPatch is deferred

AS's ThingExtensions has a static initializer that reads DefDatabase<ThingDef>.AllDefsListForReading and throws if the list is empty. Calling MpCompatPatchLoader.LoadPatch(this) in the mod constructor runs early enough that Harmony's patch installation on AS methods transitively triggers the cctor before DefDatabase is populated. The cctor throws once, the failure is cached, and every subsequent AS extension-method call rethrows TypeInitializationException — tens of thousands of errors per load. Deferring via LongEventHandler.ExecuteWhenFinished guarantees DefDatabase is loaded when Harmony touches AS.

Test plan

  • Builds cleanly (dotnet build Source/Multiplayer_Compat.csproj -c Release)
  • Deploy to D:\Steam\steamapps\workshop\content\294100\1629973374\1.6\Assemblies\Multiplayer_Compat.dll
  • Game loads cleanly (no ThingExtensions / TypeInitializationException flood)
  • Manual MP repro: host loads a save with AS containers + stored items, client joins. Previously reproducing equalizeCells shuffle desync no longer fires.
  • Visual inspection in SP — storage containers render correctly (deterministic graphic variants per container)

Notes

  • Does not depend on References/AdaptiveStorageFramework.dll — uses string-named method resolution throughout, so it stays in Source/Mods/ rather than Source_Referenced/.
  • Gate is [MpCompatFor("adaptive.storage.framework")] — patch is inert for players without AS installed.
  • Companion mod Reel's Expanded Storage (reel.expanded.storage) depends on AS Framework and shares the same code path; this patch covers it implicitly.

…r scope

AdaptiveStorage.ThingClass.InitializeStoredThings (bulk re-add on spawn) and
Notify_ItemRegisteredAtCell (per-item hook fired from AS's transpiler on
ThingGrid.RegisterInCell) both reach Verse.Graphic_Random.get_MatSingle via
MinifiedThing.get_Graphic. That consumes Verse.Rand to pick a random material
variant. If any of those calls fires inside MP's SeedMapFinalizeLoading seeded
scope, it perturbs the stream at a position the client won't reproduce,
producing divergent RoomTempTracker.equalizeCells shuffle orderings and an
eventual Rand desync via the temperature path.

Wrap both entry points with PushState(thingIDNumber)/PopState so their Rand
consumption can't leak into the outer scope. Same pattern as MP's own
SeedPawnGraphics for PawnRenderer.SetAllGraphicsDirty.

LoadPatch is deferred via LongEventHandler.ExecuteWhenFinished: AS's
ThingExtensions has a static initializer that reads DefDatabase<ThingDef>
and throws if the list is empty. Calling LoadPatch in the mod constructor
runs Harmony's patch installation early enough that it transitively triggers
the cctor before DefDatabase is populated, failing the cctor and poisoning
every subsequent AS extension-method access (thousands of errors per tick).
Deferring the install until the current LongEvent finishes guarantees
DefDatabase is loaded when Harmony touches AS.
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