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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user