Files
smartaction-spt4/Patch/Diagnostics.cs
T
funman300 8de369869f Stage 1: walk while healing legs, for SPT 4.0.13
Ground-up reimplementation of yox92/SmartAction targeting SPT 4.0.13 /
EFT 0.16.9.0.40087. Only the walk-while-healing feature is implemented;
see NOTES.md for status and the deferred sprint work.

Design departures from upstream:

- CanWalk is a guarded postfix, not a transpiler. Upstream rewrote the
  method body, which affects every MovementContext in the raid including
  bots, and is hard-incompatible with the three other mods already
  patching MovementContext in a typical load order (SAIN, ORBIT,
  maschine-DualSideDoorBreach). A postfix that only flips false -> true
  for IsYourPlayer is inert for other entities by construction.

- The postfix re-tests ObstacleCollisionFacade_1.CanMove() before
  lifting. Upstream's transpiler dropped that check along with the
  HealingLegs check, silently disabling obstacle blocking.

- Targets resolve by name via AccessTools.PropertyGetter + nameof rather
  than by GClass number, so they survive EFT version bumps.

Verified in-game against 78 loaded plugins: both patches attach, the
guard rejected 985 non-local MovementContext calls with zero leaks, and
_player was never null across ~4000 guard calls.

CanSprintPatch is parked in deferred/ rather than deleted:
MovementContext.CanSprint does not gate sprinting (EnableSprint calls
Physical.Sprint unconditionally), and forcing it true makes EnableSprint
run ForceStopInteractions()/RemoveLeftHandItem() mid-heal. Details and
the 3.11 -> 4.0.13 rename table are in NOTES.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:50:16 -07:00

88 lines
3.1 KiB
C#

using UnityEngine;
namespace SmartAction.Patch;
/// <summary>
/// Verification instrumentation for the movement patches.
///
/// These postfixes run on every movement query for every entity in the raid, so nothing here
/// may allocate or log on the hot path. Counters are plain increments; the periodic flush is
/// gated behind a call counter so Time.unscaledTime is read once per ~4096 calls rather than
/// per call. Disable via the "Diagnostics" config entry once the design is trusted.
/// </summary>
internal static class Diagnostics
{
internal static bool Enabled;
private const int TickGate = 4096;
private const float FlushIntervalSeconds = 30f;
// Guard outcomes — the multiplayer-safety evidence.
internal static long LocalCalls;
internal static long NonLocalCalls;
internal static long NullPlayerCalls;
// Patch outcomes.
internal static long WalkLifted;
internal static long WalkHeldByObstacle;
private static int _tick;
private static float _nextFlush;
// One-shot announcements, so the first occurrence of each interesting event is visible
// immediately instead of waiting up to 30s for a flush.
private static bool _saidWalkLift;
private static bool _saidWalkObstacle;
private static bool _saidNullPlayer;
internal static void OnceWalkLifted()
{
if (_saidWalkLift) return;
_saidWalkLift = true;
Plugin.Log.LogInfo("[diag] CanWalk LIFTED (HealingLegs) — first occurrence");
}
internal static void OnceWalkHeldByObstacle()
{
if (_saidWalkObstacle) return;
_saidWalkObstacle = true;
Plugin.Log.LogInfo("[diag] CanWalk HELD by obstacle check — first occurrence "
+ "(this is the CanMove() guard upstream's transpiler dropped)");
}
internal static void OnceNullPlayer()
{
if (_saidNullPlayer) return;
_saidNullPlayer = true;
Plugin.Log.LogWarning("[diag] MovementContext._player was NULL — guard handled it without throwing. "
+ "This is the predicted NRE case; it is being absorbed correctly.");
}
/// <summary>Called from the guard. Cheap on all but 1-in-4096 calls.</summary>
internal static void Tick()
{
if (++_tick < TickGate) return;
_tick = 0;
var now = Time.unscaledTime;
if (now < _nextFlush) return;
_nextFlush = now + FlushIntervalSeconds;
Flush();
}
internal static void Flush()
{
Plugin.Log.LogInfo(
$"[diag] guard: local={LocalCalls} nonLocal={NonLocalCalls} nullPlayer={NullPlayerCalls} | "
+ $"walk: lifted={WalkLifted} heldByObstacle={WalkHeldByObstacle}");
if (NonLocalCalls == 0)
{
Plugin.Log.LogWarning("[diag] nonLocal=0 — no bot/remote MovementContext has been seen yet. "
+ "Either you are not in a raid with AI, or the postfixes are not firing "
+ "for other entities. The guard is unproven until this is non-zero.");
}
}
}