# 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: ```csharp 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.