Skip to content

Commit

Permalink
Patch for UndercaveMapComponent (pit gate)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
SokyranTheDragon committed May 18, 2024
1 parent 0cf13ed commit c5b11a8
Showing 1 changed file with 60 additions and 0 deletions.
60 changes: 60 additions & 0 deletions Source/Client/Patches/Determinism.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,64 @@ private static int NewCacheTicks(int gameTick)
}
}

[HarmonyPatch(typeof(UndercaveMapComponent), nameof(UndercaveMapComponent.MapComponentTick))]
static class DeterministicUndercaveRockCollapse
{
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> 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();
}
}

}

0 comments on commit c5b11a8

Please sign in to comment.