Files
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

3.8 KiB

SmartAction — SPT 4.0.13 port notes

Target: SPT 4.0.13 / EFT 0.16.9.0.40087 / BepInEx 5.4.23.2 / netstandard2.1. Reference assemblies live in the game install; see SmartAction.csproj (SptDir property).

Build: dotnet build -c Release -p:DeployToGame=true


Stage 1 — status

Feature Status
Walk while healing legs Working — confirmed in-game and in logs
Guard rejects bots / remote players Proven — nonLocal=985 guard rejections, zero leaks
Obstacle check preserved Structurally guaranteed; never observed firing (heldByObstacle=0)
Sprint while using meds Deferred — see below

Verified 2026-07-28 with 78 other plugins loaded, including SAIN, ORBIT and maschine-DualSideDoorBreach, all three of which also patch MovementContext.


Why CanSprintPatch is deferred

deferred/CanSprintPatch.cs.txt compiles and its logic is correct, but it does not achieve the feature, and it has a side effect. Do not simply re-enable it.

MovementContext.CanSprint does not gate sprinting. The consumer is:

public void EnableSprint(bool enable)
{
    _player.Physical.Sprint(enable);        // actual sprint request — never checks CanSprint
    if (enable && CanSprint)
    {
        PlayerAnimator.AnimatedInteractions.ForceStopInteractions();
        _player.RemoveLeftHandItem();
    }
    if (!enable) RaiseChangeSpeedEvent();
}

Two consequences:

  1. Forcing CanSprint = true does not let you sprint. Physical.Sprint(bool) sets a flag (BasePhysicalClass.Sprint → Bool_2); the real gate is in the derived Physical class or the sprint input path. Not yet identified — this is the open question for Stage 2/3.
  2. In vanilla, CanSprint is false mid-heal so the ForceStopInteractions() / RemoveLeftHandItem() block never runs. Patching it to true makes that block run during a heal, which is a behavior change we did not intend.

Upstream's Patch/PatchSprint.cs uses the same condition chain as ours (a prefix rather than a postfix), so upstream did not solve this via CanSprint either. Sprint-while-consuming most likely depends on the MedController / method_8 patches that Stage 1 deliberately skipped — fold it in there.

Lesson: patching a property means inheriting every consumer's interpretation of it. CanSprint answers "may the player sprint" for one caller and "should we cancel what's in their hands" for another.


Design decisions worth keeping

  • Postfix, not transpiler/prefix. Upstream rewrote CanWalk's IL globally and used a false-returning prefix on CanSprint. Both affect every entity in the raid. A postfix that only flips false -> true, guarded on IsYourPlayer, is inert for bots by construction — and composes with SAIN/ORBIT, which are already in MovementContext.
  • Upstream's CanWalk transpiler dropped the ObstacleCollisionFacade_1.CanMove() check along with the HealingLegs check. CanWalkPatch re-tests it before lifting.
  • Resolve by name, never by GClass number. AccessTools.PropertyGetter(typeof(T), nameof(...)) survives EFT updates. Confirmed working at runtime, not just at compile time.

Rename table (3.11 → 4.0.13)

3.11 4.0.13
Player.MedsController.Class1172 Player.MedsController.ObservedMedsControllerClass
ActiveHealthController.GClass2813 ActiveHealthController.GClass3008
GClass2823_0 GClass3019_0
float_12 Float_12
medsController_0 MedsController_0
activeHealthController_0 ActiveHealthController_0
firearmsAnimator_0 unchanged — still lowercase

Trap: the property is named ObservedMedsControllerClass but its getter MethodDef is still get_Class1291_0. Use AccessTools.PropertyGetter, never the get_ string.