Desync fixes, multifaction routing, and failure surfacing#961
Desync fixes, multifaction routing, and failure surfacing#961CormacOConnor72 wants to merge 17 commits into
Conversation
|
Obviously it's a lot of changes, if anything needs clarification please let me know. |
|
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
|
Fixes
Smaller
Tests
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.
|
Fixes to wastepack events |
…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.
Fixes
Multi-faction Improvements
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.
|
Mod compat removed As requested, the mod compatibility patches are out of this PR as of 13bd053: Removed
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. |
|
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.
Once I have some more free time I'll review the PR again.
You can submit a PR to rwmt/Multiplayer-Compatibility. The main mod is generally only concerned with the base game (and dlcs) compatibility. |
|
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. |
…ayer list receive
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).
|
Merged upstream: #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. |
…ill-reset tick stamps to the world clock (rwmt#964)
|
Update: two new fixes
Fix: size the read limit for UTF-8 worst case ( The harder oneFixes #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 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.
|
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.
This PR broke Hosting Server from NewGame (converting SP save to MP save).We suspect that:
|
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
TryGetNearbyUnseenCellpatch (which only covered part of the logic). Swept the other Anomaly entities for camera-dependent tick logic: none found.FloatMenuOptionProvider_DraftedMove:PawnGotoActioncan cause desyncs #849 — drafted goto desync: ordering a drafted pawn onto its own cell went throughEndCurrentJobwithout 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.TransporterUtility.InitiateLoadingdirectly, changing loading state on the clicking client only. Now synced. (This was the "not sure if significant" TODO in SyncDelegates — it was.)dontSynccould get stuck on: an exception between theStorageSettingsClipboard.Copyprefix/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.Multifaction routing
IncidentWorker.TryExecutecatches anything that still slips through rather than silently mis-delivering it.QuestNode_GetMap, andQuestGen_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.Failure surfacing
QoL / smaller fixes
Testing status (honest)
Behavior changes