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>
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:
- Forcing
CanSprint = truedoes not let you sprint.Physical.Sprint(bool)sets a flag (BasePhysicalClass.Sprint→Bool_2); the real gate is in the derivedPhysicalclass or the sprint input path. Not yet identified — this is the open question for Stage 2/3. - In vanilla,
CanSprintisfalsemid-heal so theForceStopInteractions()/RemoveLeftHandItem()block never runs. Patching it totruemakes 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 afalse-returning prefix onCanSprint. Both affect every entity in the raid. A postfix that only flipsfalse -> true, guarded onIsYourPlayer, is inert for bots by construction — and composes with SAIN/ORBIT, which are already inMovementContext. - Upstream's
CanWalktranspiler dropped theObstacleCollisionFacade_1.CanMove()check along with theHealingLegscheck.CanWalkPatchre-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.