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,3 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
@@ -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.
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Reflection;
|
||||
using EFT;
|
||||
using HarmonyLib;
|
||||
using SPT.Reflection.Patching;
|
||||
|
||||
namespace SmartAction.Patch;
|
||||
|
||||
/// <summary>
|
||||
/// Lets you walk while healing a leg.
|
||||
///
|
||||
/// Vanilla MovementContext.CanWalk (EFT 0.16.9.0.40087):
|
||||
/// if (PhysicalConditionIs(HealingLegs)) return false;
|
||||
/// if (!ObstacleCollisionFacade_1.CanMove()) return false;
|
||||
/// return true;
|
||||
///
|
||||
/// Upstream SmartAction replaced this body with a transpiler, which rewrote the method for
|
||||
/// every MovementContext in the raid — bots included — and dropped the obstacle check along
|
||||
/// with the HealingLegs check. A postfix that only ever flips false -> true is per-instance,
|
||||
/// composes with other movement mods, needs no IL matching, and keeps CanMove() intact.
|
||||
/// </summary>
|
||||
public class CanWalkPatch : ModulePatch
|
||||
{
|
||||
protected override MethodBase GetTargetMethod()
|
||||
{
|
||||
return AccessTools.PropertyGetter(typeof(MovementContext), nameof(MovementContext.CanWalk));
|
||||
}
|
||||
|
||||
[PatchPostfix]
|
||||
private static void Postfix(MovementContext __instance, ref bool __result)
|
||||
{
|
||||
// Never grant movement vanilla already allows, and never take it away.
|
||||
if (__result) return;
|
||||
|
||||
if (!MovementPatchUtil.IsLocalPlayer(__instance)) return;
|
||||
|
||||
// Only intervene when healing is the reason we were blocked.
|
||||
if (!__instance.PhysicalConditionIs(EPhysicalCondition.HealingLegs)) return;
|
||||
|
||||
// The other vanilla blocker still applies: if something is physically in the way,
|
||||
// leave the false alone. (Null facade -> treat as blocked.)
|
||||
if (__instance.ObstacleCollisionFacade_1?.CanMove() != true)
|
||||
{
|
||||
if (Diagnostics.Enabled)
|
||||
{
|
||||
Diagnostics.WalkHeldByObstacle++;
|
||||
Diagnostics.OnceWalkHeldByObstacle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
__result = true;
|
||||
|
||||
if (Diagnostics.Enabled)
|
||||
{
|
||||
Diagnostics.WalkLifted++;
|
||||
Diagnostics.OnceWalkLifted();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SmartAction.Patch;
|
||||
|
||||
/// <summary>
|
||||
/// Verification instrumentation for the movement patches.
|
||||
///
|
||||
/// These postfixes run on every movement query for every entity in the raid, so nothing here
|
||||
/// may allocate or log on the hot path. Counters are plain increments; the periodic flush is
|
||||
/// gated behind a call counter so Time.unscaledTime is read once per ~4096 calls rather than
|
||||
/// per call. Disable via the "Diagnostics" config entry once the design is trusted.
|
||||
/// </summary>
|
||||
internal static class Diagnostics
|
||||
{
|
||||
internal static bool Enabled;
|
||||
|
||||
private const int TickGate = 4096;
|
||||
private const float FlushIntervalSeconds = 30f;
|
||||
|
||||
// Guard outcomes — the multiplayer-safety evidence.
|
||||
internal static long LocalCalls;
|
||||
internal static long NonLocalCalls;
|
||||
internal static long NullPlayerCalls;
|
||||
|
||||
// Patch outcomes.
|
||||
internal static long WalkLifted;
|
||||
internal static long WalkHeldByObstacle;
|
||||
|
||||
private static int _tick;
|
||||
private static float _nextFlush;
|
||||
|
||||
// One-shot announcements, so the first occurrence of each interesting event is visible
|
||||
// immediately instead of waiting up to 30s for a flush.
|
||||
private static bool _saidWalkLift;
|
||||
private static bool _saidWalkObstacle;
|
||||
private static bool _saidNullPlayer;
|
||||
|
||||
internal static void OnceWalkLifted()
|
||||
{
|
||||
if (_saidWalkLift) return;
|
||||
_saidWalkLift = true;
|
||||
Plugin.Log.LogInfo("[diag] CanWalk LIFTED (HealingLegs) — first occurrence");
|
||||
}
|
||||
|
||||
internal static void OnceWalkHeldByObstacle()
|
||||
{
|
||||
if (_saidWalkObstacle) return;
|
||||
_saidWalkObstacle = true;
|
||||
Plugin.Log.LogInfo("[diag] CanWalk HELD by obstacle check — first occurrence "
|
||||
+ "(this is the CanMove() guard upstream's transpiler dropped)");
|
||||
}
|
||||
|
||||
internal static void OnceNullPlayer()
|
||||
{
|
||||
if (_saidNullPlayer) return;
|
||||
_saidNullPlayer = true;
|
||||
Plugin.Log.LogWarning("[diag] MovementContext._player was NULL — guard handled it without throwing. "
|
||||
+ "This is the predicted NRE case; it is being absorbed correctly.");
|
||||
}
|
||||
|
||||
/// <summary>Called from the guard. Cheap on all but 1-in-4096 calls.</summary>
|
||||
internal static void Tick()
|
||||
{
|
||||
if (++_tick < TickGate) return;
|
||||
_tick = 0;
|
||||
|
||||
var now = Time.unscaledTime;
|
||||
if (now < _nextFlush) return;
|
||||
_nextFlush = now + FlushIntervalSeconds;
|
||||
|
||||
Flush();
|
||||
}
|
||||
|
||||
internal static void Flush()
|
||||
{
|
||||
Plugin.Log.LogInfo(
|
||||
$"[diag] guard: local={LocalCalls} nonLocal={NonLocalCalls} nullPlayer={NullPlayerCalls} | "
|
||||
+ $"walk: lifted={WalkLifted} heldByObstacle={WalkHeldByObstacle}");
|
||||
|
||||
if (NonLocalCalls == 0)
|
||||
{
|
||||
Plugin.Log.LogWarning("[diag] nonLocal=0 — no bot/remote MovementContext has been seen yet. "
|
||||
+ "Either you are not in a raid with AI, or the postfixes are not firing "
|
||||
+ "for other entities. The guard is unproven until this is non-zero.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Reflection;
|
||||
using EFT;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace SmartAction.Patch;
|
||||
|
||||
internal static class MovementPatchUtil
|
||||
{
|
||||
// MovementContext._player is protected, so it needs reflection. Resolved once at
|
||||
// type-init rather than per call — these postfixes run on every movement query.
|
||||
private static readonly FieldInfo PlayerField = AccessTools.Field(typeof(MovementContext), "_player");
|
||||
|
||||
/// <summary>
|
||||
/// True only for the player this client controls.
|
||||
///
|
||||
/// Every bot and, under Fika, every remote player owns its own MovementContext, and these
|
||||
/// postfixes fire for all of them. Gating on IsYourPlayer is what keeps the patches inert
|
||||
/// for everyone else — it is the entire multiplayer-safety story for Stage 1.
|
||||
/// </summary>
|
||||
internal static bool IsLocalPlayer(MovementContext context)
|
||||
{
|
||||
var value = PlayerField?.GetValue(context);
|
||||
|
||||
// Fast path when instrumentation is off: no counters, no branches beyond the check.
|
||||
if (!Diagnostics.Enabled)
|
||||
{
|
||||
return value is Player { IsYourPlayer: true };
|
||||
}
|
||||
|
||||
if (value is not Player player)
|
||||
{
|
||||
// Predicted failure mode: a MovementContext whose _player is not yet assigned
|
||||
// (bot mid-spawn). The pattern match absorbs it rather than throwing an NRE.
|
||||
Diagnostics.NullPlayerCalls++;
|
||||
Diagnostics.OnceNullPlayer();
|
||||
Diagnostics.Tick();
|
||||
return false;
|
||||
}
|
||||
|
||||
var isLocal = player.IsYourPlayer;
|
||||
if (isLocal) Diagnostics.LocalCalls++;
|
||||
else Diagnostics.NonLocalCalls++;
|
||||
|
||||
Diagnostics.Tick();
|
||||
return isLocal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
using SmartAction.Patch;
|
||||
|
||||
namespace SmartAction;
|
||||
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
[BepInDependency("com.SPT.core", "4.0.0")]
|
||||
public class Plugin : BaseUnityPlugin
|
||||
{
|
||||
public const string PluginGuid = "com.yox92.smartaction";
|
||||
public const string PluginName = "SmartAction";
|
||||
public const string PluginVersion = "2.0.0";
|
||||
|
||||
internal static ManualLogSource Log { get; private set; }
|
||||
|
||||
private ConfigEntry<bool> _diagnostics;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Log = Logger;
|
||||
|
||||
_diagnostics = Config.Bind(
|
||||
"Debug",
|
||||
"Diagnostics",
|
||||
true,
|
||||
"Log patch and guard activity. Counters are cheap, but this writes a summary line "
|
||||
+ "every 30s while in raid. Turn off once the behaviour is trusted.");
|
||||
|
||||
Diagnostics.Enabled = _diagnostics.Value;
|
||||
_diagnostics.SettingChanged += (_, _) => Diagnostics.Enabled = _diagnostics.Value;
|
||||
|
||||
// Stage 1 is walk-only. CanSprintPatch is deferred to Stage 2/3 — see NOTES.md:
|
||||
// MovementContext.CanSprint does not gate sprinting, and patching it caused
|
||||
// EnableSprint() to call ForceStopInteractions()/RemoveLeftHandItem() mid-heal.
|
||||
new CanWalkPatch().Enable();
|
||||
|
||||
Log.LogInfo($"{PluginName} {PluginVersion} loaded (walk-while-healing active, "
|
||||
+ $"diagnostics {(Diagnostics.Enabled ? "ON" : "off")}).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# SmartAction — SPT 4.0.x
|
||||
|
||||
Lets you **walk while healing a leg** in SPT.
|
||||
|
||||
Targets **SPT 4.0.13 / EFT 0.16.9.0.40087**, BepInEx 5, `netstandard2.1`.
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
This is a reimplementation of **[yox92/SmartAction](https://github.com/yox92/SmartAction)**,
|
||||
which targets an earlier SPT version. The feature idea, the original research into which
|
||||
EFT internals to touch, and the name are yox92's work.
|
||||
|
||||
**Upstream ships no LICENSE file.** If you intend to redistribute this or build on it,
|
||||
ask yox92 for an explicit license first — absence of a license means all rights reserved,
|
||||
and that applies to derivatives too.
|
||||
|
||||
This is not a drop-in port. The patches were rewritten rather than translated; see
|
||||
[NOTES.md](NOTES.md) for what changed and why.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
| Feature | Status |
|
||||
|---|---|
|
||||
| Walk while healing legs | Working |
|
||||
| Sprint while using meds | **Not implemented** — see NOTES.md |
|
||||
| Adaptive heal speed | Not implemented |
|
||||
| Quick-cancel surgery | Not implemented |
|
||||
|
||||
Only the first feature exists today. The rest of upstream's feature set is unported.
|
||||
|
||||
## Install
|
||||
|
||||
Drop `SmartAction.dll` into `BepInEx/plugins/SmartAction/`.
|
||||
|
||||
## Build
|
||||
|
||||
Requires the .NET SDK. Reference assemblies are read from a local SPT install:
|
||||
|
||||
```bash
|
||||
dotnet build -c Release -p:SptDir=/path/to/SPT
|
||||
```
|
||||
|
||||
Add `-p:DeployToGame=true` to copy the result straight into `BepInEx/plugins/SmartAction/`.
|
||||
|
||||
No BSG or SPT assemblies are redistributed here — the build reads them from your own install.
|
||||
|
||||
## Multiplayer / Fika
|
||||
|
||||
The patch is guarded on `IsYourPlayer` and only ever grants movement, never removes it, so
|
||||
it is inert for bots and remote players by construction. Verified in a live raid: 985 guard
|
||||
rejections of non-local `MovementContext` instances with zero leaks, alongside SAIN, ORBIT
|
||||
and maschine-DualSideDoorBreach, all of which also patch `MovementContext`.
|
||||
|
||||
No Fika dependency, no custom packets. It does not touch authoritative health state.
|
||||
|
||||
## Config
|
||||
|
||||
`BepInEx/config/com.yox92.smartaction.cfg`
|
||||
|
||||
| Setting | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `Diagnostics` | `true` | Logs patch/guard activity and a counter summary every 30s. Turn off once you trust it. |
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<AssemblyName>SmartAction</AssemblyName>
|
||||
<RootNamespace>SmartAction</RootNamespace>
|
||||
<Version>2.0.0</Version>
|
||||
<RestoreSources>https://api.nuget.org/v3/index.json</RestoreSources>
|
||||
<!-- BSG/Unity assemblies are resolved straight out of the game install.
|
||||
Override on the command line for a different install:
|
||||
dotnet build -p:SptDir=/path/to/SPT -->
|
||||
<SptDir Condition="'$(SptDir)' == ''">/mnt/games/SPT</SptDir>
|
||||
<ManagedDir>$(SptDir)/EscapeFromTarkov_Data/Managed</ManagedDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Game -->
|
||||
<Reference Include="Assembly-CSharp" HintPath="$(ManagedDir)/Assembly-CSharp.dll" Private="false" />
|
||||
<Reference Include="Comfort" HintPath="$(ManagedDir)/Comfort.dll" Private="false" />
|
||||
<Reference Include="UnityEngine" HintPath="$(ManagedDir)/UnityEngine.dll" Private="false" />
|
||||
<Reference Include="UnityEngine.CoreModule" HintPath="$(ManagedDir)/UnityEngine.CoreModule.dll" Private="false" />
|
||||
<!-- BepInEx + Harmony -->
|
||||
<Reference Include="BepInEx" HintPath="$(SptDir)/BepInEx/core/BepInEx.dll" Private="false" />
|
||||
<Reference Include="0Harmony" HintPath="$(SptDir)/BepInEx/core/0Harmony.dll" Private="false" />
|
||||
<!-- SPT -->
|
||||
<Reference Include="spt-reflection" HintPath="$(SptDir)/BepInEx/plugins/spt/spt-reflection.dll" Private="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Opt-in deploy: dotnet build -p:DeployToGame=true -->
|
||||
<Target Name="DeployToGame" AfterTargets="Build" Condition="'$(DeployToGame)' == 'true'">
|
||||
<PropertyGroup>
|
||||
<PluginDir>$(SptDir)/BepInEx/plugins/SmartAction</PluginDir>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(PluginDir)" />
|
||||
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PluginDir)" />
|
||||
<Message Importance="high" Text="Deployed $(TargetFileName) -> $(PluginDir)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Reflection;
|
||||
using EFT;
|
||||
using HarmonyLib;
|
||||
using SPT.Reflection.Patching;
|
||||
|
||||
namespace SmartAction.Patch;
|
||||
|
||||
/// <summary>
|
||||
/// Lets you sprint while using meds / healing legs.
|
||||
///
|
||||
/// Vanilla MovementContext.CanSprint (EFT 0.16.9.0.40087):
|
||||
/// if (PhysicalConditionIs(SprintDisabled)) return false;
|
||||
/// if (PhysicalConditionIs(UsingMeds)) return false; // lifted here
|
||||
/// if (PhysicalConditionIs(HealingLegs)) return false; // lifted here
|
||||
/// if (PhysicalConditionIs(OnPainkillers)) return true;
|
||||
/// if (PhysicalConditionIs(RightLegDamaged)) return false;
|
||||
/// if (PhysicalConditionIs(LeftLegDamaged)) return false;
|
||||
/// return true;
|
||||
///
|
||||
/// Upstream SmartAction used a prefix that returned false outright, suppressing the original
|
||||
/// getter for every MovementContext in the raid. That is hard-incompatible with any other mod
|
||||
/// touching sprint or stamina. This lifts exactly the two med-related blocks and replays the
|
||||
/// rest of vanilla's chain, so leg damage and SprintDisabled keep their normal meaning.
|
||||
/// </summary>
|
||||
public class CanSprintPatch : ModulePatch
|
||||
{
|
||||
protected override MethodBase GetTargetMethod()
|
||||
{
|
||||
return AccessTools.PropertyGetter(typeof(MovementContext), nameof(MovementContext.CanSprint));
|
||||
}
|
||||
|
||||
[PatchPostfix]
|
||||
private static void Postfix(MovementContext __instance, ref bool __result)
|
||||
{
|
||||
if (__result) return;
|
||||
|
||||
if (!MovementPatchUtil.IsLocalPlayer(__instance)) return;
|
||||
|
||||
// Not ours to override — this is a deliberate game/quest/effect lockout.
|
||||
if (__instance.PhysicalConditionIs(EPhysicalCondition.SprintDisabled))
|
||||
{
|
||||
if (Diagnostics.Enabled) Diagnostics.SprintHeldBySprintDisabled++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only intervene when meds or leg-healing is what blocked us.
|
||||
var blockedByMeds = __instance.PhysicalConditionIs(EPhysicalCondition.UsingMeds)
|
||||
|| __instance.PhysicalConditionIs(EPhysicalCondition.HealingLegs);
|
||||
if (!blockedByMeds) return;
|
||||
|
||||
// Replay the remainder of vanilla's chain so a broken leg still stops you.
|
||||
if (__instance.PhysicalConditionIs(EPhysicalCondition.OnPainkillers))
|
||||
{
|
||||
__result = true;
|
||||
if (Diagnostics.Enabled)
|
||||
{
|
||||
Diagnostics.SprintLifted++;
|
||||
Diagnostics.OnceSprintLifted();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (__instance.PhysicalConditionIs(EPhysicalCondition.RightLegDamaged)
|
||||
|| __instance.PhysicalConditionIs(EPhysicalCondition.LeftLegDamaged))
|
||||
{
|
||||
if (Diagnostics.Enabled)
|
||||
{
|
||||
Diagnostics.SprintHeldByLeg++;
|
||||
Diagnostics.OnceSprintHeldByLeg();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
__result = true;
|
||||
|
||||
if (Diagnostics.Enabled)
|
||||
{
|
||||
Diagnostics.SprintLifted++;
|
||||
Diagnostics.OnceSprintLifted();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user