From 50e159ce1e2febfc467ec51322984f427b124921 Mon Sep 17 00:00:00 2001 From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:37:30 +0200 Subject: [PATCH] Patch for UndercaveMapComponent (pit gate) (#470) Info on `UndercaveMapComponent:MapComponentTick` and why it needs patching: - RNG calls after current map/visible camera area checks - Majority of the RNG calls are visual/audio effects only - `TriggerCollapseFX` does have an effect on simulation by spawning collapsed mountain roof - The collapsing rocks have minimal impact on the game, as they avoid player pawns Fixing RNG is easy by just pushing/popping the state. Fixing the collapsed mountain roof is more complex. Possible solutions: - Disable rock collapse completely - Disable current map checks (suboptimal for performance) - Disable the existing calls to `TriggerCollapseFX` and call it in a deterministic way (used in this PR) Information about the patch: - The transpiler will make the call to `Rand.MTBEventOccurs` always fail, making so `TriggerCollapseFX` is never called - This could be further improved by removing the RNG call and the current map check altogether to improve performance, but would be more complex - The prefix and postfix push/pop RNG state, making sure the ticking code doesn't mess with the RNG state - The postfix re-implements the call to `TriggerCollapseFX` in a deterministic way - If the current player is not looking at the current map, it will be called with `0` as both arguments to prevent additional effects from triggering - The call to the method is surrounded by RNG push/pop state, as the amount of RNG calls will differ if the player is not looking at the map - It's safe as the simulation-affecting RNG calls happen first - The call is currently unseeded, but could be easily seeded with `Gen.HashCombineInt(Find.TickManager.TicksGame, __instance.map.uniqueID)` if we care about having a seed here Remaining issues with `UndercaveMapComponent`/Pit Gate: - The rock collapse has additional check to not drop rocks on player faction pawns. However, I assume this won't work properly with Multifaction, which could end up crushing pawns of some players. --- Source/Client/Patches/Determinism.cs | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 1eac7ec5..b6b8a7b2 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -524,4 +524,64 @@ static IEnumerable Transpiler(IEnumerable inst } } + [HarmonyPatch(typeof(UndercaveMapComponent), nameof(UndercaveMapComponent.MapComponentTick))] + static class DeterministicUndercaveRockCollapse + { + static IEnumerable Transpiler(IEnumerable instr) + { + var target = MethodOf.Lambda(Rand.MTBEventOccurs); + + foreach (var ci in instr) + { + yield return ci; + + // Add "& false" to any call to Rand.MTBEventOccurs. + // We'll handle those calls in our postfix. + if (ci.Calls(target)) + { + yield return new CodeInstruction(OpCodes.Ldc_I4_0); + yield return new CodeInstruction(OpCodes.And); + } + } + } + + static void Prefix() => Rand.PushState(); + + static void Postfix(UndercaveMapComponent __instance) + { + // Pop the RNG state from the prefix + Rand.PopState(); + + // Make sure the pit gate is collapsing + if (__instance.pitGate is not { IsCollapsing: true }) + return; + + // Check if the rocks should collapse + var mtb = UndercaveMapComponent.HoursToShakeMTBTicksCurve.Evaluate(__instance.pitGate.TicksUntilCollapse / 2500f); + if (!Rand.MTBEventOccurs(mtb, 1, 1)) + return; + + // Since the number of RNG calls will depend on numDustEffecters argument, we need to push/pop the RNG state. + // The RNG calls related to simulation will happen first, followed by the one determined by amount of + // effecters - it would not be MP safe, but since it happens last it will be fine once we pop the state. + Rand.PushState(); + + // If not looking at the map, trigger the collapse without shake/effecters (since it's not needed for current player). + // The call to play a sound is handled by RW itself, since it targets a specific map already. + if (Find.CurrentMap != __instance.map) + { + // Progress the RNG state, matching the RandomInRange call in other two cases + Rand.RangeInclusive(0, 100); + __instance.TriggerCollapseFX(0, 0); + } + // Else, follow vanilla shake/effecter rules + else if (__instance.pitGate.CollapseStage == 1) + __instance.TriggerCollapseFX(UndercaveMapComponent.StageOneShakeAmount, UndercaveMapComponent.StageOneNumCollapseEffects.RandomInRange); + else + __instance.TriggerCollapseFX(UndercaveMapComponent.StageTwoShakeAmount, UndercaveMapComponent.StageTwoNumCollapseEffects.RandomInRange); + + Rand.PopState(); + } + } + }