Skip to content

Desync fixes, multifaction routing, and failure surfacing#961

Open
CormacOConnor72 wants to merge 17 commits into
rwmt:devfrom
CormacOConnor72:pr/fixes-for-dev
Open

Desync fixes, multifaction routing, and failure surfacing#961
CormacOConnor72 wants to merge 17 commits into
rwmt:devfrom
CormacOConnor72:pr/fixes-for-dev

Conversation

@CormacOConnor72

@CormacOConnor72 CormacOConnor72 commented Jul 19, 2026

Copy link
Copy Markdown

Desync fixes, multifaction quest/incident routing, and failure surfacing

This PR bundles fixes developed and play-tested on a local fork, targeting several long-standing issues. Organized by area — happy to split any part into separate PRs if that's easier to review.

Desync / correctness fixes

  • Desync caused by Anomaly DLC UnnaturalCorpse: wrong random state / trace hashes match but RNG diverges (3 desync reports) #959 — UnnaturalCorpse desyncs: the corpse's teleport/escape logic checks whether the local camera can see it, so clients watching it simulated differently from clients who weren't. It's now treated as unobserved during simulation, the same approach as the existing TryGetNearbyUnseenCell patch (which only covered part of the logic). Swept the other Anomaly entities for camera-dependent tick logic: none found.
  • ⁠ Certain Anomaly psychic rituals not working in multiplayer ⁠ #855 — psychic rituals silently fail: vanilla bug, still in 1.6.4871 — the Horax invocation toil modifies an inventory container while iterating it, throws, and the ritual outcome never fires. The container is snapshotted first now. Also fixes singleplayer.
  • FloatMenuOptionProvider_DraftedMove:PawnGotoAction can cause desyncs #849 — drafted goto desync: ordering a drafted pawn onto its own cell went through EndCurrentJob without being synced. It's now a SyncMethod like the rest of ordered-job handling, with a recheck at execution time since the command arrives a few ticks after the click.
  • Carry-to-shuttle unsynced: the float-menu option called TransporterUtility.InitiateLoading directly, changing loading state on the clicking client only. Now synced. (This was the "not sure if significant" TODO in SyncDelegates — it was.)
  • dontSync could get stuck on: an exception between the StorageSettingsClipboard.Copy prefix/postfix pair disabled syncing for the rest of the session. Added finalizer support to the MpPatch framework and converted that site, plus the 11 ThingFilter/work-tab cleanup postfixes where a mid-draw exception permanently broke the tab.
  • Latent desync in multifaction faction creation (Biotech): map generation registered custom xenotypes into the global database from state only the creating client has, so clients disagreed on the database — and on later random rolls that scale with its size. The path now no-ops identically on all clients. Trade-off: those starting pawns' custom xenotypes aren't added to the database; the pawns keep their xenotype.
  • Quest ticks aborted: a quest ending during its own tick removed itself from the list being iterated, throwing and skipping the remaining quests that pass. Deterministic, so not a desync — but quests silently lost ticks. Index loop now.
  • Runtime-created Lords unresolvable: only Lords from the save were registered for cross-referencing, so synced jobs pointing at a Lord created mid-session failed to resolve and ran lord-less. They now register on add/remove.
  • "Referenced but not deep-saved" spam (vanilla 1.6 bug): Ideology tree-connection sightings reference wild trees the save compressor doesn't know about, so the trees compress away and the references null on load. They're declared to the compressor now. Also fixes singleplayer saves.

Multifaction routing

  • Events are triggering on my friends colony instead of mine and same for them. #659 / supersedes Incorrect map for multifaction events fix #775 — wrong-faction events: every faction's storyteller ran against every map, so players received each other's raids and letters, and each colony got event pressure multiplied by player count (likely behind the "events too frequent" reports). A faction's storyteller now only targets maps it owns; unowned site maps go to factions with people present. A logged safety net in IncidentWorker.TryExecute catches anything that still slips through rather than silently mis-delivering it.
  • Quest pawn arrived in "Spectator" faction #546 — quest pawns join Spectator, and quest routing generally: quests had no owning faction, so quest logic ran in whatever faction context the tick happened to hold — usually the spectator's — and reward pawns joined Spectator, ideo-dependent generation used the wrong ideo, letters went to the wrong players. Quests now carry an owner (saved with the game, backfilled for old saves): assigned at generation, updated when a player accepts, and all quest ticking/signals/cleanup runs in the owner's context.
  • Quest map selection respects the owner — all pickers: several code paths pick "a player home map" with no faction check, so one faction's quest could bind another player's colony (refugees arriving on the wrong plot was a live report). The shared pickers (QuestNode_GetMap, and QuestGen_Get.GetMap, used by refugee/beggar/wanderer and the DLC arrival quests) and two private ones (work sites, reliquary pilgrims) now all restrict to the generating faction's maps. Re-delivery when a quest's map is lost also prefers the owner's maps — previously it took the first colony in the list, which could send a hostile mech cluster to an uninvolved player.
  • Quest rewards: any player could claim another faction's quest choice, and it executed in the clicker's context. Claiming is now gated to the owner and runs in the owner's context.
  • World-tick context leaks: world-ticked code runs as the spectator faction, so vanilla features gated on "does this belong to the player" silently never fired for anyone: festivals and other date rituals, funeral obligations for caravan deaths, ideology role letters, caravan royals' bestowing ceremonies, and caravan children's growth moments (auto-rolled with no letter and no choice). Each of these paths now runs under the relevant player faction, and the resulting letters reach that faction instead of being dropped.
  • Growth-moment choices are faction-scoped: the choice dialog used to force-open for every player at timeout, and anyone could set another faction's child's traits. Sessions now have an owner; only that faction's players see the dialog and make the choice. Also fixed a crash for caravan children — relevant outside multifaction too.
  • Rituals: the ritual action runs in the organizer's faction context, and ideo-reform completion works from ritual toils, not only from direct commands.
  • Per-faction Anomaly/mech state: bossgroup escalation (call counts, cooldowns, kills) and study/analysis progress were global — one player's Diabolus summon or chip analysis affected every faction. Both are per-faction now.
  • Per-faction map data cleanup: zone cells under a newly spawned building, and designations on destroyed or recruited things, are now cleaned up in every faction's copy of the map data, not just the currently active one. Hooked only at those objective events — player actions like Cancel still affect only that player's own designations.
  • Player orders always execute: the safety net above was also eating deliberate orders — calling Empire aid while standing on an ally's map consumed the permit favor but suppressed the raid. Incidents fired from a synced player command now bypass the net.
  • Naming prompts: the faction/settlement rename dialogs are synced.
  • Custom ideologies for joining players: an ideology exported from singleplayer (.rid) can be used at faction creation; it travels with the creation command and is reconstructed identically on every client. (Reusing the live ideo-editor page was rejected — it mutates live game state.)

Failure surfacing

  • Failed patches/registrations at startup now show a confirmation dialog before hosting or joining, instead of only a tooltip marker.
  • Exceptions in synced commands/ticks are still caught-and-continued, but each one is a potential divergence — they're now counted per session, surfaced to the player once, shown in the dev overlay, and included in desync reports as a "Simulation Failures" section.
  • Unsynced-spawn tripwire: when a Thing spawns outside the synced simulation — the signature of an unsynced mod action, and exactly how both mod desyncs above started — MP now warns once per def with the stack that names the offender. Turns that class of bug from a debugging session into one log line.
  • Better desync reports: the attached log now keeps the newest lines (it previously kept the first 10,000 and cut off the desync itself); reports record whether the players' files matched when they connected; and connecting anyway with mismatched files posts a chat warning. Also corrected the mismatch window's "RW version match" label — it actually compares Multiplayer versions.
  • The desync report parser writes placeholders for unreadable sections instead of dropping them, and cross-ref registration failures warn once per type instead of silently dropping objects.

QoL / smaller fixes

  • 45 seconds of real time before the session pauses for a client that's behind (was ~1.5 seconds of game time, which tabbed-out clients tripped constantly); returning clients catch up as before.
  • Convert-to-singleplayer cleans up spectator-faction references first — converted saves were logging dangling faction errors.
  • Skip timestamp fixing for destroyed pawns (removed a save warning on every predator kill).
  • Ultrafast speed button shown in dev mode (the hotkey and vote support already existed).
  • Config comparison ignores line-ending differences (fixes cross-platform "configs don't match").
  • Fixed a rare divide-by-zero and negative ETA in the connecting window's download display.
  • Game ref updated to 1.6.4871.

Testing status (honest)

  • Builds clean against 1.6.4871 refs; 164/164 tests pass.
  • All patch targets and lambda ordinals verified against the decompiled 1.6.4871 assembly.
  • Running in regular live sessions with friends (multifaction, Royalty/Ideology/Biotech, ~20 mods). The desyncs we hit are diagnosed from their reports and fixed above — still playing and fixing as we go.

Behavior changes

  • Event pacing in multifaction drops to per-faction rates (colonies previously got N storytellers' attention).
  • The UnnaturalCorpse can act while on screen in MP (determinism over the vanish-when-unseen conceit).
  • The pause gate tolerates 45s of a client being behind before pausing everyone.
  • Anomaly study/analysis and bossgroup escalation become per-faction in multifaction.
  • Custom xenotypes of multifaction starting pawns aren't added to the xenotype database (the pawns keep their xenotype).
  • Festivals, caravan funerals, and caravan children's growth choices now actually occur in multifaction — they silently never fired before.
  • Growth-moment dialogs only appear for (and are decidable by) the child's faction.
  • Deliberately player-ordered incidents execute even when targeting another player faction's map.

@CormacOConnor72

Copy link
Copy Markdown
Author

Obviously it's a lot of changes, if anything needs clarification please let me know.

@CormacOConnor72

Copy link
Copy Markdown
Author

Working on additional fix for harmony bug I found, do not merge.

- Guard null ideo development when reconstructing custom ideos (non-fluid
  .rid ideos have no development tracker)
- Gate the bossgroup copy-in on multifaction so plain MP keeps the vanilla
  component live instead of stamping a stale cooldown over it each map tick
- Reset the behind-timer when a player leaves Playing status so a rejoin
  or desync gets a fresh pause grace, with unit tests covering the timer
  lifecycle (PauseGraceTest)
- Exclude the spectator faction from ritual session owner resolution
- Normalize lone carriage returns in the synced config comparison
@CormacOConnor72

Copy link
Copy Markdown
Author

Fixes

  • FactionCreator.ReconstructCustomIdeo NRE'd on non-fluid custom ideos — vanilla only creates the IdeoDevelopmentTracker for fluid ideos, and the external-ideo scribe path omits the node, so loading a standard .rid at faction creation threw inside the synced command on every client. Now null-guarded, with the same defensive guard in FixIdeoAfterCopy.
  • Bossgroup cooldown was silently erased in non-multifaction MP: AsyncTimeComp.PreContext force-pushes the map faction before every map tick, so the per-faction bossgroup copy-in stamped the load-time cooldown over the live GameComponent_Bossgroup, while the write-back postfix only ran under multifaction. The copy-in is now gated to match — plain MP leaves the vanilla component untouched.
  • The 45s pause-grace timer wasn't reset when a player left the Playing state with it armed (desync or rejoin), so a returning player could trigger one spurious global pause. The reset lives in the evaluation loop rather than the status setter because a rejoin only changes connection state, never player status.

Smaller

  • Ritual session owner resolution now excludes the spectator faction (its def is PlayerColony, so a bare IsPlayer check matches it)
  • Config comparison also normalizes lone \r, not just CRLF

Tests

  • The pause-grace evaluation is extracted from the server loop and covered by new NUnit cases (PauseGraceTest), including regression tests for the rejoin and desync paths — both fail against the previous code. Suite is at 164 passing.

Ready for review now.

Fixes from the first multiplayer group session on this branch:

- Blood Animations compat: isolate its effect rand rolls from the synced
  stream and suppress its filth spawns in MP (settings-gated and
  render-path driven; the cause of every desync in the session's reports)
- Declare Ideology TreeSighting references before save compression so
  sighted wild trees stop being compressed away (vanilla 1.6 gap)
- Quest cache modification during quest ticking: a quest ending during
  its own tick removes itself from the async cache mid-enumeration
- Quest map selection: multifaction quests no longer bind another player
  faction's map (refugees arrived on the wrong colony)
- Register runtime-created lords in the shared cross-ref directory so
  synced ceremony jobs resolve their lord
…ETA race

- VNPE's Extract and Drain gizmos mutate simulation state in interface
  context on the clicking client only (confirmed desync source via desync
  report traces); register their entry points as synced methods, resolved
  by reflection and skipped when VNPE or Multiplayer Compatibility make
  that appropriate.
- Warn once per def, with the offending stack, when a Thing spawns outside
  the synced simulation or with an interface-block id - turns this class
  of mod bug from a forensics session into a one-line diagnosis.
- Fix a check-then-use race on the connecting window's recomputed download
  speed (observed DivideByZeroException; -1 sentinel passed the old check).
- Desync reports kept the first 10,000 log lines and discarded the tail -
  the desync itself and everything near it. Keep a startup head plus the
  newest lines within the same budget.
- Record the join-data diff string on the session when a player connects
  anyway through the mismatch window, include it in desync reports, and
  post a chat warning on file/modlist mismatches - whether the builds
  matched is the first question of any desync investigation.
- Correct the diff string's 'RW version match' label: it compares
  Multiplayer mod versions.
… C# quest map picks

- Zone-overlap clears and destroy/recruit-time designation removal now
  repeat across every faction's manager copy. Hooked at the objective call
  sites, not RemoveAllDesignationsOn itself, which also backs player-intent
  designators where repeating would delete other factions' designations.
- QuestGen_Get.GetMap - the C# quest-root map picker used by refugee,
  beggar, wanderer and DLC arrival quests - now applies the same
  generating-faction owner rule as QuestNode_GetMap.
- Faction creation cleared of client-local xenotype-database writes - the
  game-global database (and later factionless-pawn generation Rand rolls)
  stayed identical across clients only for the issuer before.
- Ideo ticking, member-death notifications and birthdays now run under the
  relevant player faction's context instead of the spectator's: date
  obligations (festivals), funeral obligations for caravan/cross-map
  deaths, role letters and caravan children's growth-moment choices all
  fire again, with letters routed to the right faction.
- Growth-moment sessions are scoped to the child's faction (dialog,
  choices, timeout) and tolerate unspawned pawns instead of throwing.
- Quest routing: owner filters for two private map pickers (work sites,
  reliquary pilgrims), retargeting on map loss prefers the quest owner's
  maps, bestowing-ceremony generation runs under the royal's faction, and
  owner inference also reads pawn look targets.
- Incidents fired inside a synced player command bypass the cross-faction
  suppression net - explicit orders always execute.
…forensics improvements

- Wastepack goodwill and retaliation now attribute to the dumping faction
  (holder chain, call-site context, scribed per-thing stamps); retaliation
  quests target the dumper's map and never fall back to an arbitrary
  player's. Unknown dumpers drop retaliation with a warning.
- Timed site exits and defeat-all-enemies site quests tick under the map
  owner's faction context instead of the spectator (pawn-loss and reward
  misfire fixes).
- Season notifications are computed per player faction from that faction's
  own min-timezone home map clock against per-faction saved state, fixing
  a desync where clients' season transitions diverged under async time
  (also affected spectating clients in single-faction games).
- PlayerItemAccessibilityUtility no longer serves frame-cached results in
  multiplayer: the RealTime.frameCount cache key diverged per client and
  fed stale data into quest generation and the AI-core offer roll.
- The host's game language now travels with the join data (protocol 57)
  and shows in the mismatch window: translated text is scribed, so
  mismatched languages diverge saves.
@CormacOConnor72

Copy link
Copy Markdown
Author

Fixes to wastepack events
Fixed desyncing on async seasons.(trade off is a cosmetic shared "season has started")
Fixed map and timed events not ticking at the right speed.
Language fixes
Fixed weird item desync

…x version nag

- TurnItOnandOff keeps unsaved per-client state and per-client settings;
  host/joiner power drift forks rand in PowerNet battery shuffle
  (repeatable tester desyncs). Compat clears its state on every client at
  every MP (re)load and carries host settings via the game comp.
- Map.IsPlayerHome spectator override now UI-only: in ticking/command
  contexts it returned per-client results (spectator world tick).
- Version update warning only on exact continuous builds; git describe on
  branches containing the tag made downstream builds nag testers to
  "update" onto upstream.
- Natural goodwill drift and ideo-derived opinion caps ran against the
  spectator faction only (Faction.OfPlayer during the world tick): player
  factions kept permanent grudges and caps were computed against an ideo
  nobody plays. Each ownable faction now has its own GoodwillSituationManager
  and per-NPC drift timers; drift and the 1000-tick caps recalc fan out per
  faction under its context. Vanilla's pairwise Faction.relations supplies
  the storage.
- Goodwill cache fills from UI context no longer persist (call-time worker
  values differed per client until the next recalc while sim reads trusted
  them, and persisting fills fired relation-kind flips from UI context).
- Shared ideos: reformation applies only for the primary follower faction;
  ritual repeat-penalty windows are per faction.
@CormacOConnor72

CormacOConnor72 commented Jul 21, 2026

Copy link
Copy Markdown
Author

Fixes

  • Spectator watching is safer. A recent upstream change made the game answer "is this a player's home base?" differently on a spectator's screen than everyone else's, which could desync multifaction games. Spectator view is now cosmetic only.
  • Removed the false "update available" popup that told people running this build to "upgrade" to the older official version for users testing. This is only for old versions.

Multi-faction Improvements

  • Faction reputation is now tracked per player faction. Previously in multifaction games, NPC faction opinion (grudges fading over time, ideology-based opinion limits) were silently computed against a faction nobody plays so grudges never healed and reputation-locked content stayed locked forever. Each player faction now has its own working relationship with every NPC faction.
  • Sharing a religion no longer lets players step on each other. With a shared ideoligion, one player could reform the religion out from under the other, and one player's ritual burned everyone's ritual cooldown. Reforms are now owned by one faction, and ritual cooldowns are tracked per faction. Everything else about a shared religion is intentionally still shared — same church, same beliefs.

Bug reports with desync files welcome as always.

Blood Animations, Vanilla Nutrient Paste Expanded, and Turn It On and
Off compat move out of this PR at maintainer request; they stay on the
mod-compat branch for re-merge once a home is decided. Also removes the
TIOAO host-settings plumbing from the game comp and save/reload path.
Saves written with compat builds keep an inert tioaoHostSettings node.
@CormacOConnor72

CormacOConnor72 commented Jul 21, 2026

Copy link
Copy Markdown
Author

Mod compat removed

As requested, the mod compatibility patches are out of this PR as of 13bd053:

Removed

  • Blood Animations isolated rand state + filth suppression
  • Vanilla Nutrient Paste Expanded synced Extract/Drain gizmos
  • Turn It On and Off state reset on (re)load + host settings via game comp

Also removes the TIOAO host-settings plumbing (game comp field + save/load hooks). Nothing else in the PR depended on them; still 164/164 on tests.

They live on mod-compat (https://github.com/CormacOConnor72/Multiplayer/tree/mod-compat) for anyone who might need them all three fixed desyncs.

@mibac138

Copy link
Copy Markdown
Member

This is a big PR and I don't have enough time to review it fully right now. I picked out some changes that are simple and look good. Please create new PRs for those changes (preferably one for each to keep it organized) and I'll approve them.

  • Introduction of Multiplayer.LoadingError (it'd be great if you could add a screenshot of how it looks in-game)
  • Update of Rimworld.Ref dependency
  • Download speed calculation fix in ConnectingWindow
  • LogGenerator - I wanted to do that one some time ago already myself but never got to it :)
  • UserReadableDesyncInfo

Once I have some more free time I'll review the PR again.

They live on mod-compat (https://github.com/CormacOConnor72/Multiplayer/tree/mod-compat) for anyone who might need them all three fixed desyncs.

You can submit a PR to rwmt/Multiplayer-Compatibility. The main mod is generally only concerned with the base game (and dlcs) compatibility.

@CormacOConnor72

Copy link
Copy Markdown
Author

Sure, I'll break it up so its more digestible.

I will say my priority are the improvements and fixes. If have some spare time at the weekend, I'll break up the PRs based on types of fixes, improvements, multi faction, which dlc, etc

I have a doc for each set of commits so doing it that way saves me time.

Feedback.cs: kept this branch's PawnGotoAction EndCurrentJob sync, which matches
upstream rwmt#957 but adds a transpiler call-count check and only ends the pawn's
job if it is still the Goto this order targeted (the synced command runs ticks
after the click, so the pawn may have started an unrelated job since).
@CormacOConnor72

CormacOConnor72 commented Jul 22, 2026

Copy link
Copy Markdown
Author

Merged upstream:
#962 JoinDataWindow NRE fix (captures session.connector before the session is nulled). Code was already null-guarded so it composes fine.

#957 This synced PawnGotoAction's direct EndCurrentJob call... which I fixed differently.

Mine has a fail-loud call-count guard plus a stale-job.

The current dev main can technically end an unrelated job that the pawn started in the ticks between click and command execution.

@CormacOConnor72

CormacOConnor72 commented Jul 22, 2026

Copy link
Copy Markdown
Author

Update: two new fixes

  1. Steam display names over 32 UTF-8 bytes kill the session for everyoneServerPlayerListPacket binds steamPersonaName with Constants.k_cwchPersonaNameMax as the limit, but that Steamworks constant counts characters (cwch = count of wide chars) while ByteReader.ReadString enforces bytes, and the writer never clamps. A legal 30-char Cyrillic name is 57 bytes → every receiver of the player list throws PacketReadException (including the host via its local loopback) and the session tears down. ~17+ Cyrillic/Greek chars, ~11+ CJK, or a few emoji trigger it and it also fires off Steam friend nicknames, since GetFriendPersonaName returns the caller's nickname for that friend when one is set.

Fix: size the read limit for UTF-8 worst case (* 4).

The harder one

Fixes #964 — cooldowns misbehave in async time.

All three reported surfaces (shuttle cooldown, daily skill growth reset, ability/skill cooldowns) are the same bug: an absolute tick stamped under one map's clock and compared under another's after the thing crosses maps or enters the world — and world/caravan context never swaps a clock in at all, so those reads compare against whatever map ticked last. Fix: stamp and read the affected state against the async world clock, which runs at the fastest map's speed and only pauses when everything pauses, so it's a monotone session clock defined in every context.

Covers Ability.cooldownEndTick (single-reader funnel through CooldownTicksRemaining — psycasts, charges, gizmos all covered), the skill tracker's 30k midnight-reset guard, and CompLaunchable.lastLaunchTick, plus worldTicks now being scribed (older saves fall back to the previous rebuild-from-TicksGame).

In non-async games all clocks are lockstep, so they are unaffected + SP is untouched.

TO BE CLEAR: under async, cooldowns elapse in session time rather than per-map time — with per-map clocks there's no single "correct" elapsed time for an object that changes clocks, and session time is the only basis that's coherent in every context.

This also aligned this functionality with the rest of the async stuff.

With async time, a player whose map is paused while a peer's map runs fast saw
rapid UI flicker, pawns teleporting instead of moving, and a degrading world
render, worsening over time and clearing on unpause. No desync - it is entirely
client-side. Two maps at different non-zero speeds do not trigger it; only a
paused one does.

AsyncWorldTimeComp.PreContext set Find.TickManager.CurTimeSpeed and PostContext
never restored it, unlike AsyncTimeComp which is snapshot-balanced. Since the
world's speed is the maximum over all maps with a non-zero rate, the fastest
map's speed stayed installed globally for the rest of the frame. TickRatePatch
replaces the TickRateMultiplier getter and computes from that global, while the
Paused getter is patched from Find.CurrentMap, so the same frame reported
Paused = true and TickRateMultiplier = 3 - a pair vanilla cannot produce.
Per-frame code believes both: tweeners extrapolate against a root position that
never advances and snap back, and UI animates at the remote map's rate against a
stopped clock. Most per-frame paths (WindowStackOnGUI, MainButtonsOnGUI,
World.WorldUpdate, WorldRenderer) install no time context at all, which is why
the world render is affected too.

PreContext now saves the speed and PostContext restores it, and TickPatch's
Postfix installs the viewed context's speed for the remainder of the frame so
those unwrapped paths read something defined rather than a leftover. ExposeData
reads the world's own DesiredTimeSpeed instead of the global, which only held
the world's speed because of the unrestored write.

Speed only: ticksGameInt stays the world clock and gameStartAbsTick is untouched
because AsyncTimeComp.GameStartAbsTick latches it into scribed per-map state,
where a viewer-dependent latch would create a desync. The install runs after the
whole tick and command loop and every sim path installs its own context on
entry, so it is never a simulation input - tick counts, ordering and rand draws
are unchanged.

Also adds a Find.CurrentMap null guard to the Paused postfix and prints
TickRateMultiplier beside Paused in the dev overlay, where the contradiction is
directly visible.
… comp

Follow-up to the previous commit. That change made the global
Find.TickManager.CurTimeSpeed describe whatever the local player is looking at,
so that rendering and the UI have a defined context. CreateAsyncTimeCompForMap
read the same global into a newly generated map's DesiredTimeSpeed, which is
scribed synced state, so clients viewing different things - the planet view, a
colony map, or no map at all - would compute different starting speeds for the
same generated map and scribe the difference. A session-level pause zeroing one
map's rate while the world's maximum stays non-zero makes those branches
disagree even when every speed looks identical.

Only the non-async branch is affected. It now takes the world's
DesiredTimeSpeed, which is what the global carried there before and is identical
on every client. The usingMapTimeFromSingleplayer read at the same site runs
during host-time save conversion on a single machine, before any client exists,
and is left alone.
@CormacOConnor72

Copy link
Copy Markdown
Author

UI flickering and weird map rendering was caused by the game calling the pause state differently due to a change I made to make a consistent world state.

This should be fixed now. While annoying it was entirely client-side and cosmetic.

The DesiredTimeSpeed scribe added alongside the paused-map time context fix
used the label "timeSpeed", which the timeSpeedInt node three lines above
already used. Two Scribe_Values.Look calls sharing a label write two
<timeSpeed> nodes on save, and on load both resolve through
curXmlParent[label] to the first child, so both reads returned timeSpeedInt
and the world's desired speed was never persisted or restored.

Renamed to "globalTimeSpeed" with timeSpeedInt as the default. Look skips
writing a value equal to its default, so the node appears only when the two
differ, and saves without it (older saves, and the duplicate-node saves the
collision produced) fall back to the speed loaded on the line above rather
than to Paused.

Both values are deterministic synced world state, so the wrong value was
wrong consistently on every client - a correctness and save-hygiene defect,
not a divergence.
@simonlsp

Copy link
Copy Markdown

This PR broke Hosting Server from NewGame (converting SP save to MP save).

We suspect that:

  • HostUtil.SetupGameFromSingleplayer
    • MapSetup.SetupMap
      • Current Dev Branch: startingTimeSpeed = Find.TickManager.CurTimeSpeed (Safe)
      • This PR: Iterate through AsyncTime (Including Map not ready for Multiplayer)
      • Result: Null Object Exception

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.

3 participants