Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
198 commits
Select commit Hold shift + click to select a range
675ec35
Isolate RNG in sound and music entry points
WoodyWizard Oct 17, 2026
f8d9f49
Run gravship takeoff/landing end in a synced command
WoodyWizard Oct 17, 2026
83d0cc8
Fix inverted check in the zone/plan cell shuffle patch
WoodyWizard Oct 17, 2026
d2cf042
Keep the server join flow on the server thread
WoodyWizard Oct 17, 2026
393ac90
Allow PriorityWork.Clear during ticks
WoodyWizard Oct 17, 2026
93d6f86
Make the deferred stack tracer fail safely
WoodyWizard Oct 17, 2026
059510e
Keep trace hash depth independent of the tick loop call site
WoodyWizard Oct 17, 2026
fa7f328
Use deterministic job-loop counter for pawns without a map
WoodyWizard Oct 17, 2026
873f837
Tick the UI tile temperature cache from the interface
WoodyWizard Oct 17, 2026
7c88a31
Fix init data requests that hang every later join
WoodyWizard Oct 17, 2026
a59e1c8
Restore watched sync fields even if the watched code throws
WoodyWizard Oct 17, 2026
9a7c278
Complete path jobs right after PathFinderTick
WoodyWizard Oct 17, 2026
dd969f9
Reject server-only commands and foreign faction changes from clients
WoodyWizard Oct 17, 2026
b6f1126
Balance faction context pushes in trading UI and map faction ticks
WoodyWizard Oct 17, 2026
22f958b
Don't mutate live sessions while saving; fix inverted checks
WoodyWizard Oct 17, 2026
2aec6ac
Reset sync state flags in finalizers instead of postfixes
WoodyWizard Oct 17, 2026
e3edd35
Drop partially received fragmented packets on state change
WoodyWizard Oct 17, 2026
7d09265
Keep one globally ordered command log for joiners
WoodyWizard Oct 17, 2026
85066f3
Keep thing filter edits out of the sender-only simulation
WoodyWizard Oct 17, 2026
ec1900b
Validate command size before adding it to the log
WoodyWizard Oct 17, 2026
de1999a
Make growth moment sessions survive the child leaving the map
WoodyWizard Oct 17, 2026
dce32db
Sync colonist bar reordering with pawns resolved on the sender
WoodyWizard Oct 17, 2026
6e8249c
Apply deterministic HashCode.Combine patches early and always
WoodyWizard Oct 17, 2026
dba4ff2
Validate remote lengths before allocating
WoodyWizard Oct 17, 2026
ee6e9af
Look up portal/transporter sessions by their target, fix forming faction
WoodyWizard Oct 17, 2026
5238392
Keep sync serializer reader and writer aligned
WoodyWizard Oct 17, 2026
e0161aa
Don't run caravan splitting commands through the local dialog
WoodyWizard Oct 17, 2026
3f41f92
Recache trade deals in ticks and commands instead of the UI
WoodyWizard Oct 17, 2026
7b666d7
Save VTR player counts and clamp the world count
WoodyWizard Oct 17, 2026
b2fb3f1
Time out unresponsive players and clamp client timing reports
WoodyWizard Oct 17, 2026
1d5e411
Emit valid default values in the sync method transpiler
WoodyWizard Oct 17, 2026
3b62baa
Find the map gen faction from any player caravan
WoodyWizard Oct 17, 2026
619bc19
Run sim long events in a deterministic context
WoodyWizard Oct 17, 2026
c3889fa
Apply the context faction to every map in forced faction pushes
WoodyWizard Oct 17, 2026
8205395
Catch Multiplayer build and def type differences when joining
WoodyWizard Oct 17, 2026
2e921f5
Make fragile determinism transpilers fail loudly
WoodyWizard Oct 17, 2026
578c0a6
Create factions with deterministic faction data on every client
WoodyWizard Oct 17, 2026
8bc60df
Sync world pawns and genes reliably, fix sync worker edge cases
WoodyWizard Oct 17, 2026
318bac4
Include the selected world tile in the WorldSelected sync context
WoodyWizard Oct 17, 2026
f5a41e8
Make Rand push/pop exception safe
WoodyWizard Oct 17, 2026
c189a0c
Stop the join flow of players who left while waiting for init data
WoodyWizard Oct 17, 2026
109b5f2
Log leaked field watch scopes only once
WoodyWizard Oct 17, 2026
1b011ec
Give the init data upload a longer timeout
WoodyWizard Oct 17, 2026
bcf9384
Don't return null motes only on non-issuing clients
WoodyWizard Oct 17, 2026
bdf1995
Make join points atomic with a designated uploader
WoodyWizard Oct 17, 2026
8c2731e
Keep faction creation pages from changing shared game state
WoodyWizard Oct 17, 2026
5a92b1d
Isolate issuer-only code in commands from shared state
WoodyWizard Oct 17, 2026
5923645
Judge UI stat cache staleness with the thing's own time
WoodyWizard Oct 17, 2026
6c48789
Detect desyncs on standalone servers
WoodyWizard Oct 17, 2026
a95832b
Keep the letter stack the same on every client in multifaction
WoodyWizard Oct 17, 2026
d900efe
Save the pending gravship cutscene end
WoodyWizard Oct 17, 2026
977e1bc
Guard the UI tile temperature tick against exceptions
WoodyWizard Oct 17, 2026
d67d559
Reload the game under ProgramState.MapInitializing
WoodyWizard Oct 17, 2026
8a091e5
Don't let temporary faction generation register ideoligions
WoodyWizard Oct 17, 2026
0bd4e06
Test that current-format command logs are never reordered
WoodyWizard Oct 17, 2026
b7a389f
Null-guard the multifaction letter helpers
WoodyWizard Oct 17, 2026
5452502
Don't check the child's map in GrowthMomentSession.IsSessionValid
WoodyWizard Oct 17, 2026
7973914
Merge fix/cmdlog: single ordered command log and synchronized join po…
WoodyWizard Oct 17, 2026
38fbe9d
Don't let one subsystem's exception skip the rest of a tick
WoodyWizard Oct 17, 2026
7e07030
Merge fix/net: server-thread join flow, packet bounds, command valida…
WoodyWizard Oct 17, 2026
45c536a
Merge fix/determinism: gravship sync, deterministic hashes, patch fixes
WoodyWizard Oct 17, 2026
3f575f8
Keep pending sync opinions across reloads
WoodyWizard Oct 17, 2026
9427b7d
Run the ship countdown end in the world context
WoodyWizard Oct 17, 2026
b33e913
Switch the faction's world data for map generation
WoodyWizard Oct 17, 2026
c436e65
Address review findings in the sync fixes
WoodyWizard Oct 17, 2026
4425065
Merge fix/sync: exception-safe field watches, serializer fixes, deter…
WoodyWizard Oct 17, 2026
389678c
Harden SimLongEventContext capture and exit
WoodyWizard Oct 17, 2026
0030d36
Use the right faction for map gen world data and the ship launch
WoodyWizard Oct 17, 2026
9fb2dcd
Add small safety checks to the tracer and command context
WoodyWizard Oct 17, 2026
d55dcf2
Merge fix/rng: RNG isolation, robust stack tracing, long-event sim co…
WoodyWizard Oct 17, 2026
2da45bb
Restore the exact faction data on forced pops, skip removed maps
WoodyWizard Oct 17, 2026
4649662
Save the trade deal dirty state and ignore dirtying while loading
WoodyWizard Oct 17, 2026
ad4bc2f
Open other factions' expiring letters without their UI
WoodyWizard Oct 17, 2026
a1e0059
Use MapHeld for growth moment children, end portal sessions with the …
WoodyWizard Oct 17, 2026
513af76
Generate a new faction's ideoligion for its own faction def
WoodyWizard Oct 17, 2026
3027e16
Merge fix/sessions: shared letter stack, deterministic faction contex…
WoodyWizard Oct 17, 2026
d6a13aa
Keep the faction creation event's faction data in its sim context
WoodyWizard Oct 17, 2026
8b2268a
Force the gravship faction push in the synced cutscene ends
WoodyWizard Oct 17, 2026
83f8853
Isolate the remaining client-subset session windows
WoodyWizard Oct 17, 2026
66ee8bc
Send desync traces fragmented
WoodyWizard Oct 17, 2026
940a691
Don't time out join points while the uploader is saving
WoodyWizard Oct 17, 2026
2756769
Merge fix/integ-factions: integration fixes for faction contexts and …
WoodyWizard Oct 17, 2026
44e28cd
Merge fix/integ-join: integration fixes for traces and join point tim…
WoodyWizard Oct 17, 2026
37302bd
Keep the world data for joiners within the fragmented packet limit
WoodyWizard Oct 17, 2026
edd7d3c
Start one join point per saved tick for save requests
WoodyWizard Oct 17, 2026
2947cb7
Discard saved standalone command logs of other protocols
WoodyWizard Oct 17, 2026
ac8feeb
Open the trade window in a local only scope
WoodyWizard Oct 17, 2026
36c2e34
Merge fix/followups: world data size bound, save dedupe, protocol-tag…
WoodyWizard Oct 17, 2026
c877df8
Let the arbiter run in-game synchronous long events
WoodyWizard Oct 18, 2026
41c8fab
Count UTF-8 bytes in packet string limits, bound array lengths
WoodyWizard Oct 18, 2026
ab816c3
Replay debug actions under the acting player's world view
WoodyWizard Oct 18, 2026
1095720
Don't tick an accepted async world quest twice per tick
WoodyWizard Oct 18, 2026
6605f20
Make unnatural corpse teleports independent of the camera
WoodyWizard Oct 18, 2026
43a6493
Notify seasons per player faction in multifaction
WoodyWizard Oct 18, 2026
035d208
Sync InitiateLoading called from float menu options
WoodyWizard Oct 18, 2026
822b44c
Let map time contexts nest, guard maps without time comps
WoodyWizard Oct 18, 2026
8ce4386
Handle Steam reconnects and free closed P2P sessions
WoodyWizard Oct 18, 2026
0b4fde8
Keep multifaction storytellers off other player factions' maps
WoodyWizard Oct 18, 2026
fb00a6e
Don't re-enter the map context for nested incident execution
WoodyWizard Oct 18, 2026
f3b6ca5
Offer the growth moment window only to the child's faction
WoodyWizard Oct 18, 2026
244f8ef
Guard FixIdeoAfterCopy against non-fluid ideos and run it in ticks
WoodyWizard Oct 18, 2026
cd6d1f5
Stop the server once, on its own thread
WoodyWizard Oct 18, 2026
64e3aef
Keep the grav engine cache out of the interface's reach
WoodyWizard Oct 18, 2026
6b109e5
Don't report sends to a disconnecting LiteNet peer as errors
WoodyWizard Oct 18, 2026
7399c62
Keep other players out of trade requests and ritual goodwill
WoodyWizard Oct 18, 2026
e2a589f
Clear local starting pawn requests before faction map generation
WoodyWizard Oct 18, 2026
1c20d17
Guard null maps and jobs in three patches
WoodyWizard Oct 18, 2026
6ad2ef2
Count the host's view for VTR, reset viewer counts of earlier sessions
WoodyWizard Oct 18, 2026
123ee05
Judge animal hunger alerts with the animal's own time
WoodyWizard Oct 18, 2026
d4349f2
Ask before hosting or joining with loading errors
WoodyWizard Oct 18, 2026
dceac58
Clear zone cells under new buildings for every faction
WoodyWizard Oct 18, 2026
6dc1e5b
Remove other factions' designations on a destroyed thing
WoodyWizard Oct 18, 2026
a21fb66
Count skipped simulation errors and report them
WoodyWizard Oct 18, 2026
c5aac4f
Drop the spectator's destination reservations when converting to SP
WoodyWizard Oct 18, 2026
77fb479
Keep the end of the log and the join data diff in desync reports
WoodyWizard Oct 18, 2026
c6c9fa6
Snapshot the invoker's inventory when holding psychic ritual offerings
WoodyWizard Oct 18, 2026
89dfd7a
Fix pawn timestamps without saving destroyed pawns
WoodyWizard Oct 18, 2026
c3c3323
Read the download speed once in the connecting window
WoodyWizard Oct 18, 2026
0198180
Ignore line endings when comparing synced mod configs
WoodyWizard Oct 18, 2026
b815de5
Seed quest name generation retries
WoodyWizard Oct 18, 2026
5005f8f
Sync the product style of bills
WoodyWizard Oct 18, 2026
92af9a9
Warn about a game language mismatch when joining
WoodyWizard Oct 18, 2026
f8dd311
Send big commands fragmented
WoodyWizard Oct 18, 2026
46638cc
Keep quest map picks to the generating faction's maps
WoodyWizard Oct 18, 2026
e05a679
Refill the ritual role group cache before a synced TryAssignAnyRole
WoodyWizard Oct 18, 2026
9b94384
Split selections over 200 things into several packets
WoodyWizard Oct 18, 2026
d3c7bb1
Don't fill the goodwill situation cache from the interface
WoodyWizard Oct 18, 2026
f409a3d
Run caravan battle wins and ideo ticks for their player faction
WoodyWizard Oct 18, 2026
0abd01f
Skip season notifications of player factions without a home
WoodyWizard Oct 18, 2026
9b5661a
Key the item accessibility cache by simulation step in MP
WoodyWizard Oct 18, 2026
417c0ac
Reset the manual priorities marker in a finalizer
WoodyWizard Oct 18, 2026
d412a1c
Unwind leaked faction context pushes at tick and command boundaries
WoodyWizard Oct 18, 2026
af91582
Register lords in the shared cross refs
WoodyWizard Oct 18, 2026
017ed3c
Recheck the goto job before ending it from a synced drafted move
WoodyWizard Oct 19, 2026
324df63
Make the standalone join point test able to fail
WoodyWizard Oct 19, 2026
49cd635
Don't prompt again right after rejecting a Steam join request
WoodyWizard Oct 19, 2026
799efd4
Close due Steam sessions after reading the tick's packets
WoodyWizard Oct 19, 2026
60b457b
Reset the simulation error count with the world from the server
WoodyWizard Oct 19, 2026
ce0136a
Take the season with the home map's whole clock
WoodyWizard Oct 19, 2026
2da2603
Look only at the faction's own homes for its season
WoodyWizard Oct 19, 2026
81ba184
Show a faction's season message only to its players
WoodyWizard Oct 19, 2026
bec7f23
Document the shared ideo limit of the ideo tick context
WoodyWizard Oct 19, 2026
c181b79
Shift the timestamps of all of a destroyed pawn's abilities
WoodyWizard Oct 19, 2026
546b68d
Skip the battlefield owner push after the battle is won
WoodyWizard Oct 19, 2026
65bfd9d
Follow the viewed map for VTR every frame instead of catching transit…
WoodyWizard Oct 19, 2026
40dc4c0
Merge fix/pr-net: Steam reconnects, server stop, fragmented commands,…
WoodyWizard Oct 19, 2026
4d6eba2
Merge fix/pr-mf: multifaction seasons, grav engine cache, quest maps,…
WoodyWizard Oct 19, 2026
5373330
Switch map contexts for nested world tick incidents on another map
WoodyWizard Oct 19, 2026
aa636c5
End only the goto a drafted stop order was aimed at
WoodyWizard Oct 19, 2026
25558d4
Check unnatural corpse reservations of every player faction
WoodyWizard Oct 19, 2026
64c25c4
Limit the psychic ritual offering fix to MP and the required count
WoodyWizard Oct 19, 2026
a3e2414
Derive the debug command's world view from its context
WoodyWizard Oct 19, 2026
c5d0566
Restore the map of a non-forced faction push when unwinding
WoodyWizard Oct 19, 2026
a6a4ab7
Remove the spectator through vanilla's faction removal when convertin…
WoodyWizard Oct 19, 2026
524e05e
Count pocket map owners and all held pawns for multifaction storytellers
WoodyWizard Oct 19, 2026
a3ccabd
Treat sim long events as simulation steps for the item accessibility …
WoodyWizard Oct 19, 2026
6bc0940
Merge fix/pr-desync: desync fixes ported from open upstream PRs
WoodyWizard Oct 19, 2026
5fb7c98
Tick story watcher adaptation once per game tick
WoodyWizard Oct 19, 2026
f59d6b7
Pick interface melee verbs without building verb entries
WoodyWizard Oct 19, 2026
17c7354
Don't filter quest threat generators in single player
WoodyWizard Oct 19, 2026
46c06a7
Fire multifaction quest threats from one storyteller per map
WoodyWizard Oct 19, 2026
03f3ae5
Ignore line endings in mod XML file hashes
WoodyWizard Oct 19, 2026
201829a
Offer saving the game after a disconnect
WoodyWizard Oct 19, 2026
db58b65
Explain disconnects caused by a server state exception
WoodyWizard Oct 19, 2026
783d500
Load synced starting pawns like vanilla loads pawns
WoodyWizard Oct 19, 2026
0d00fcc
Stop collecting desync traces after the stack walk throws
WoodyWizard Oct 19, 2026
b9ad395
Stop the launch destination targeter when the launchable is gone
WoodyWizard Oct 19, 2026
291c3c4
Tick the world and maps in lockstep without async time
WoodyWizard Oct 19, 2026
3d8a29a
Restore the gravship landing pause after loading
WoodyWizard Oct 19, 2026
6472678
Start rituals with the starting player's role assignments
WoodyWizard Oct 19, 2026
52cd03d
Make FastTileFinder.Closest deterministic
WoodyWizard Oct 19, 2026
3682aea
Read the gravship move designator through its lazy accessor
WoodyWizard Oct 19, 2026
016815f
Pause when a caravan meeting or demand dialog opens
WoodyWizard Oct 19, 2026
bdb2515
Place the landing gravship in its map's context
WoodyWizard Oct 19, 2026
44a946a
Time extended camera shakes with the MP timer
WoodyWizard Oct 19, 2026
0d73a38
Only click the ritual gizmo the player clicked
WoodyWizard Oct 19, 2026
1fa6df1
Sync the dev remove hediff button of the health tab
WoodyWizard Oct 19, 2026
ab76273
Warn when Prepatcher didn't patch the ritual role widget
WoodyWizard Oct 19, 2026
6c5bf26
Mark traces unreliable before logging a stack walk failure
WoodyWizard Oct 19, 2026
03d89bd
Match vanilla more closely when picking interface melee verbs
WoodyWizard Oct 19, 2026
5e4e77e
Merge fix/is-ux: interface melee verb lag, mod XML hashes, save after…
WoodyWizard Oct 19, 2026
6fcaecb
Report not paused while a world or map tick runs
WoodyWizard Oct 19, 2026
34e19f8
Start new maps at the game's speed without async time
WoodyWizard Oct 19, 2026
9992f50
Hide the per-client GameInitData while reading a starting pawn
WoodyWizard Oct 19, 2026
e47be47
Keep the landing marker on the confirming client until it lands
WoodyWizard Oct 19, 2026
74a851a
Set the speed of every tickable after landing without async time
WoodyWizard Oct 19, 2026
2bca475
End gravship cutscenes that lost their gravship
WoodyWizard Oct 19, 2026
425c18d
Apply only the widget-changed role assignments at ritual start
WoodyWizard Oct 19, 2026
218c7e5
Land the gravship with the confirming player's rotation
WoodyWizard Oct 19, 2026
f145f9a
Don't let the simulation use a tile cache the interface regenerated
WoodyWizard Oct 19, 2026
2b952b9
Remove the departing gravship in its map's context
WoodyWizard Oct 19, 2026
2336353
Report the simulated paused state to commands and sim long events
WoodyWizard Oct 19, 2026
99d1da2
Fire other quest incident makers once and keep owners without data fi…
WoodyWizard Oct 19, 2026
925534c
Start maps generated anywhere in the world tick at the maps' tick
WoodyWizard Oct 19, 2026
255df8c
Merge fix/is-story: storyteller adaptation, quest threats, starting p…
WoodyWizard Oct 19, 2026
273b61b
Ignore late gravship aborts and stale cutscene freeze state
WoodyWizard Oct 19, 2026
7f555bb
Merge fix/is-odyssey: gravship landing state, ritual role snapshots, …
WoodyWizard Oct 19, 2026
48afdb4
Describe the desync review changes for the pull request
WoodyWizard Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 143 additions & 26 deletions Source/Client/AsyncTime/AsyncTimeComp.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using HarmonyLib;
using Multiplayer.Client.AsyncTime;
using Multiplayer.Client.Comp;
using Multiplayer.Client.Factions;
using Multiplayer.Client.Patches;
Expand Down Expand Up @@ -64,18 +65,9 @@ public TimeSpeed DesiredTimeSpeed

public int TickableId => map.uniqueID;

public int GameStartAbsTick
{
get
{
if (gameStartAbsTickMap == 0)
{
gameStartAbsTickMap = Find.TickManager?.gameStartAbsTick ?? 0;
}

return gameStartAbsTickMap;
}
}
// No side effect: latching the global value on first read took whichever clock was set at that moment (another
// map's under async time). Saves without the field get the world's value when loading.
public int GameStartAbsTick => gameStartAbsTickMap != 0 ? gameStartAbsTickMap : Find.TickManager?.gameStartAbsTick ?? 0;

public Map map;
public int mapTicks;
Expand All @@ -93,9 +85,11 @@ public int GameStartAbsTick
// Shared random state for ticking and commands
public ulong randState;

// Always empty: map commands are queued in AsyncWorldTimeComp.cmds to keep a single global order
public Queue<ScheduledCommand> cmds = new();

public int CurrentPlayerCount { get; private set; }
public void ResetPlayerCount() => CurrentPlayerCount = 0;
public int VTR => CurrentPlayerCount > 0 ? VTRSync.MinimumVtr : VTRSync.MaximumVtr;

public AsyncTimeComp(Map map, int gameStartAbsTick = 0)
Expand All @@ -111,13 +105,26 @@ public AsyncTimeComp(Map map, int gameStartAbsTick = 0)
public void Tick()
{
tickingMap = map;
int randDepth = Rand.stateStack.Count;
PreContext();
int factionDepth = FactionContext.stack.Count;

//SimpleProfiler.Start();

try
{
map.MapPreTick();
// Like vanilla's DoSingleTick, an exception in one subsystem shouldn't skip the rest of the tick
// (in particular the mapTicks increment)
try
{
map.MapPreTick();
}
catch (Exception e)
{
Log.Error($"Exception in MapPreTick of {map}: {e}");
SimulationFailures.Record("MapPreTick", e);
}

mapTicks++;
Find.TickManager.ticksGameInt = mapTicks;

Expand All @@ -131,12 +138,53 @@ public void Tick()
tickListRare.Tick();
tickListLong.Tick();

TickMapSessions();
try
{
TickMapSessions();
}
catch (Exception e)
{
Log.Error($"Exception ticking sessions of {map}: {e}");
SimulationFailures.Record("map sessions", e);
}

Find.Storyteller.StorytellerTick();
Find.StoryWatcher.StoryWatcherTick();
try
{
Find.Storyteller.StorytellerTick();
}
catch (Exception e)
{
Log.Error(e.ToString());
SimulationFailures.Record("storyteller", e);
}

QuestManagerTickAsyncTime();
// The world tick (vanilla DoSingleTick) already ticks the story watcher once per game tick, ticking it
// here too made adaptation and pop adaptation grow once more per map. Only resolve the colonists downed
// during this map tick, which vanilla does later in the same tick.
StoryWatcherMapTick.downedOnly = true;
try
{
Find.StoryWatcher.StoryWatcherTick();
}
catch (Exception e)
{
Log.Error(e.ToString());
SimulationFailures.Record("story watcher", e);
}
finally
{
StoryWatcherMapTick.downedOnly = false;
}

try
{
QuestManagerTickAsyncTime();
}
catch (Exception e)
{
Log.Error(e.ToString());
SimulationFailures.Record("async quests", e);
}

map.MapPostTick();
Find.TickManager.ticksThisFrame = 1;
Expand All @@ -148,6 +196,8 @@ public void Tick()
}
finally
{
RestoreRandStack(randDepth + 1, this);
FactionContext.RestoreStack(factionDepth, this);
PostContext();
Multiplayer.game.sync.TryAddMapRandomState(map.uniqueID, randState);
eventCount++;
Expand All @@ -174,7 +224,18 @@ public void UpdateManagers()
map.glowGrid.GlowGridUpdate_First();
}

private TimeSnapshot? prevTime;
private readonly struct ContextFrame(TimeSnapshot? prevTime, int outerRandDepth, bool continuesOuter)
{
public readonly TimeSnapshot? prevTime = prevTime;
public readonly int outerRandDepth = outerRandDepth;
public readonly bool continuesOuter = continuesOuter;
}

// Contexts nest (e.g. a quest's time getter enters its map's context while that map ticks the quest), so each
// PreContext keeps its own previous time. A single field made the outer PostContext restore the map's time
// instead of the world's, and the world clock then jumped to the map's.
private readonly Stack<ContextFrame> contextFrames = new();
private int contextRandDepth = -1;

public void PreContext()
{
Expand All @@ -184,33 +245,73 @@ public void PreContext()
: Multiplayer.WorldComp.spectatorFaction,
force: true);

prevTime = TimeSnapshot.GetAndSetFromMap(map);
// Directly inside this map's own context: go on with its live random state instead of restarting from
// the state saved when the outer context began
bool continuesOuter = contextFrames.Count > 0 && Rand.stateStack.Count == contextRandDepth;
if (continuesOuter)
randState = Rand.StateCompressed;

contextFrames.Push(new ContextFrame(TimeSnapshot.GetAndSetFromMap(map), contextRandDepth, continuesOuter));

Rand.PushState();
Rand.StateCompressed = randState;
contextRandDepth = Rand.stateStack.Count;

// Reset the effects of SkyManager.SkyManagerUpdate
map.skyManager.curSkyGlowInt = map.skyManager.CurrentSkyTarget().glow;
}

public void PostContext()
{
prevTime?.Set();
if (contextFrames.Count == 0)
{
Log.ErrorOnce($"Multiplayer: PostContext without PreContext on {map}", 0x2C7B90 ^ map.uniqueID);
return;
}

var frame = contextFrames.Pop();

frame.prevTime?.Set();

randState = Rand.StateCompressed;
Rand.PopState();
if (frame.continuesOuter)
Rand.StateCompressed = randState;
contextRandDepth = frame.outerRandDepth;

map.PopFaction();
}

// A Rand push left unpopped by an exception would otherwise be saved as the context's state, orphan the
// context's own entry and keep the stack too deep (which disables desync traces) for the rest of the session
public static void RestoreRandStack(int depth, object context)
{
int count = Rand.stateStack.Count;
if (count == depth) return;

// Too shallow (an extra pop) can't be repaired, only reported
Log.ErrorOnce($"Multiplayer: unbalanced Rand push/pop in {context}: stack depth {count}, expected {depth}", 0x5A3D17 ^ depth);

while (Rand.stateStack.Count > depth)
Rand.PopState();
}

public void ExposeData()
{
Scribe_Values.Look(ref mapTicks, "mapTicks");
Scribe_Values.Look(ref timeSpeedInt, "timeSpeed");

Scribe_Values.Look(ref gameStartAbsTickMap, "gameStartAbsTickMap");
if (Scribe.mode == LoadSaveMode.LoadingVars && gameStartAbsTickMap == 0)
gameStartAbsTickMap = Find.TickManager.gameStartAbsTick;

Scribe_Custom.LookULong(ref randState, "randState", 1);

// Affects the simulation through VTR. Saved so that it survives join point reloads and joiners start
// with the same counts as everyone else.
var playerCount = CurrentPlayerCount;
Scribe_Values.Look(ref playerCount, "playerCount");
CurrentPlayerCount = playerCount;
}

public int IncreasePlayerCount() => CurrentPlayerCount += 1;
Expand All @@ -220,11 +321,9 @@ public void ExposeData()

public void FinalizeInit()
{
cmds = new Queue<ScheduledCommand>(
Multiplayer.session.dataSnapshot?.MapCmds.GetValueSafe(map.uniqueID) ?? new List<ScheduledCommand>()
);

Log.Message($"Init map with cmds {cmds.Count}");
// Map commands live in the world's queue (see MultiplayerSession.ScheduleCommand) which is built only in
// Loader.PostLoad. Rebuilding a queue here (possibly on the loading thread, or for a map generated during
// play) used to drop or duplicate commands.
}

public static bool keepTheMap;
Expand All @@ -246,8 +345,10 @@ public void ExecuteCmd(ScheduledCommand cmd)
TickPatch.currentExecutingCmdIssuedBySelf = cmd.IsIssuedBySelf() && !TickPatch.Simulating;
TickPatch.currentExecutingCmdType = cmdType;

int randDepth = Rand.stateStack.Count;
PreContext();
map.PushFaction(cmd.GetFaction(), force: true);
int factionDepth = FactionContext.stack.Count;

context.map = map;

Expand Down Expand Up @@ -291,6 +392,7 @@ public void ExecuteCmd(ScheduledCommand cmd)
catch (Exception e)
{
MpLog.Error($"Map cmd exception ({cmdType}): {e}");
SimulationFailures.Record($"map command {cmdType}", e);
}
finally
{
Expand All @@ -304,8 +406,19 @@ public void ExecuteCmd(ScheduledCommand cmd)
Find.Selector.selected = prevSelected;
prevSelected = null;

Find.MainButtonsRoot.tabs.Notify_SelectedObjectDespawned();
// Local UI, depends on the local selection
try
{
using (LocalOnlyScope.Enter())
Find.MainButtonsRoot.tabs.Notify_SelectedObjectDespawned();
}
catch (Exception e)
{
Log.Error($"Exception updating main tabs after a command: {e}");
}

RestoreRandStack(randDepth + 1, this);
FactionContext.RestoreStack(factionDepth, this);
map.PopFaction();
PostContext();

Expand Down Expand Up @@ -411,6 +524,10 @@ void RestoreState()
var mode = SyncSerialization.ReadSync<DesignatorMode>(data);
var designator = SyncSerialization.ReadSync<Designator>(data);

// A sync worker returns null when its designator's target is gone (e.g. Designator_MoveGravship once the
// landing started or was aborted, which happens in commands, so on every client at the same point)
if (designator == null) return;

try
{
if (!SetState(designator)) return;
Expand Down
Loading