8de369869f
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>
89 lines
3.8 KiB
Markdown
89 lines
3.8 KiB
Markdown
# 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.
|