fix #270: fire ReportExhaustion on the stamina-exhaustion edge only, not every stats tick
Retail calls CPhysicsObj::report_exhaustion from exactly one site - CommandInterpreter::HandleExhaustion (0x006b3c70), a notification handler for the stamina-exhaustion EVENT. Campaign P P1 wired it to every movement-stats application instead (every stamina regen/drain tick), and each call re-dispatches the current movement state through the animation sink - truncating any in-flight action animation. The diagnostic session log shows 490 spurious casting-stance re-queues in one short session: 'sometimes stuck in spell animations' was every stamina tick that collided with a cast gesture's play window. The re-apply now fires only when the exhausted state (stamina == 0) transitions, matching retail's event semantics. Stats still reach PlayerWeenie immediately via RuntimeMovementSkillProjection.ApplyTo. Also adds the [remote-edge] probe (rides ACDREAM_DUMP_MOTION=1): one line per remote HitGround/LeaveGround - each such edge drains the mover's pending action animations (retail HandleEnterWorld), the working theory for intermittently missing monster attack swings. Complete Release suite: 10,026 passed / 5 skips / 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5788fdaa02
commit
a46c8e65b2
4 changed files with 101 additions and 10 deletions
|
|
@ -98,6 +98,38 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #270 — Stuck spell animations + intermittently missing monster attack animations
|
||||
|
||||
**Status:** LOCAL CAUSE FIXED 2026-07-30 (pending user re-test); remote
|
||||
attack-animation misses under investigation with the [remote-edge] probe
|
||||
**Severity:** HIGH (combat/casting presentation)
|
||||
**Component:** motion re-dispatch cadence / remote action animations
|
||||
|
||||
**Local stuck casts — root cause CONFIRMED and fixed:** Campaign P P1 wired
|
||||
`ApplyMovementStats` to call `MotionInterpreter.ReportExhaustion()` on EVERY
|
||||
movement-stats application — i.e. every stamina regen/drain tick. Each call
|
||||
re-dispatches the current movement state through the animation sink,
|
||||
truncating any in-flight action animation; the diagnostic session log shows
|
||||
490 spurious casting-stance re-queues while the user was in magic mode.
|
||||
Retail fires `CPhysicsObj::report_exhaustion` from exactly ONE site —
|
||||
`CommandInterpreter::HandleExhaustion` (0x006b3c70), a notification handler
|
||||
invoked on the stamina-EXHAUSTION EVENT. Fix: the re-apply now fires only
|
||||
when the exhausted state (stamina == 0) transitions; skills/burden/stamina
|
||||
still reach `PlayerWeenie` immediately (the next natural dispatch picks up
|
||||
rate changes, exactly retail).
|
||||
|
||||
**Monster attack misses — theory + probe:** every logged attack UM
|
||||
(cmd=0x00D3, spd=2.0) DID dispatch (`SetCycle 0x400000D3`), so the wire →
|
||||
animation path works. Working theory: remote ground-contact edges (bounce
|
||||
hops / contact flickers on dead-reckoned bodies) each drain the mover's
|
||||
pending action animations (retail `HandleEnterWorld` semantics — faithful
|
||||
per edge, wrong if we generate edges retail doesn't). A `[remote-edge]`
|
||||
probe (rides `ACDREAM_DUMP_MOTION=1`) now logs every remote
|
||||
HitGround/LeaveGround with the guid — one repro session with a missed swing
|
||||
will convict or acquit.
|
||||
|
||||
---
|
||||
|
||||
## #269 — Slope-stop slide runs too far (post-bounce-rework residual)
|
||||
|
||||
**Status:** OPEN (user live gate 2026-07-30 — "almost pass with merits")
|
||||
|
|
|
|||
|
|
@ -290,13 +290,33 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the current <see cref="RuntimeMovementSkillState"/>
|
||||
/// snapshot (skills/burden/stamina) to the live player controller and
|
||||
/// forces an immediate movement re-evaluation via
|
||||
/// <c>MotionInterpreter.ReportExhaustion</c> — the retail
|
||||
/// <c>CMotionInterp::ReportExhaustion</c> dual-dispatch re-apply, now
|
||||
/// wired to a real consumer (Campaign P Slice P1).
|
||||
/// Tracks the previous "stamina exhausted" state so
|
||||
/// <see cref="ApplyMovementStats"/> can fire retail's exhaustion
|
||||
/// notification on the EDGE only. Null = no stamina reading applied yet.
|
||||
/// </summary>
|
||||
private bool? _lastStaminaExhausted;
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the current <see cref="RuntimeMovementSkillState"/>
|
||||
/// snapshot (skills/burden/stamina) to the live player controller.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Stuck-cast fix (2026-07-30): retail fires
|
||||
/// <c>CPhysicsObj::report_exhaustion</c> from exactly ONE site —
|
||||
/// <c>CommandInterpreter::HandleExhaustion</c> (0x006b3c70), a
|
||||
/// notification-handler vtable slot invoked on the stamina-exhaustion
|
||||
/// EVENT — not on every vitals refresh. The P1 wiring called
|
||||
/// <c>ReportExhaustion()</c> on EVERY movement-stats application
|
||||
/// (every stamina regen/drain tick), and each call re-dispatches the
|
||||
/// current movement state through the animation sink — truncating any
|
||||
/// in-flight action animation (cast gestures wedged mid-play; the
|
||||
/// diagnostic session showed 490 spurious stance re-queues). The
|
||||
/// re-apply now fires only when the exhausted state (stamina == 0)
|
||||
/// actually TRANSITIONS, matching retail's event semantics. Skill/
|
||||
/// burden changes still reach <see cref="PlayerWeenie"/> immediately
|
||||
/// via <see cref="RuntimeMovementSkillProjection.ApplyTo"/> — the next
|
||||
/// natural dispatch picks up the new rates, exactly as retail.
|
||||
/// </remarks>
|
||||
private void ApplyMovementStats(string reason)
|
||||
{
|
||||
PlayerMovementController? controller = _player.Controller.Controller;
|
||||
|
|
@ -307,9 +327,16 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
return;
|
||||
}
|
||||
|
||||
controller!.Motion.ReportExhaustion();
|
||||
|
||||
RuntimeMovementSkillSnapshot snapshot = _domain.Character.MovementSkills.Snapshot;
|
||||
bool exhausted = snapshot.CurrentStamina == 0;
|
||||
if (_lastStaminaExhausted != exhausted)
|
||||
{
|
||||
bool isEdge = _lastStaminaExhausted is not null;
|
||||
_lastStaminaExhausted = exhausted;
|
||||
if (isEdge)
|
||||
controller!.Motion.ReportExhaustion();
|
||||
}
|
||||
|
||||
_log(
|
||||
$"player: applied server movement {reason} "
|
||||
+ $"run={snapshot.RunSkill} jump={snapshot.JumpSkill} "
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ public static class PhysicsDiagnostics
|
|||
public static bool ProbeCellEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Stuck-cast/missing-attack investigation (2026-07-30). When true,
|
||||
/// every REMOTE ground-contact edge (HitGround / LeaveGround — each of
|
||||
/// which drains the mover's pending action animations via retail's
|
||||
/// <c>HandleEnterWorld</c>) emits one <c>[remote-edge]</c> line with the
|
||||
/// server guid. Correlates eaten attack animations with spurious
|
||||
/// contact flickers. Rides <c>ACDREAM_DUMP_MOTION=1</c> so one flag
|
||||
/// captures the whole animation story.
|
||||
/// </summary>
|
||||
public static bool DumpMotionEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// L.2d slice 1 (2026-05-13). When true, every BSP-shadow-entry hit
|
||||
/// attributed by <c>TransitionTypes.FindObjCollisions</c> emits a
|
||||
|
|
|
|||
|
|
@ -747,6 +747,26 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
rm.Body.Position = resolved.Position;
|
||||
if (resolved.CellId != 0)
|
||||
committedCellId = resolved.CellId;
|
||||
// [remote-edge] probe (stuck-cast/missing-attack investigation,
|
||||
// 2026-07-30): each ground edge drains the mover's pending
|
||||
// action animations (retail HandleEnterWorld) — one line per
|
||||
// edge correlates eaten attack gestures with contact flickers.
|
||||
Action hitGround = rm.Movement.HitGround;
|
||||
Action leaveGround = rm.Motion.LeaveGround;
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.DumpMotionEnabled)
|
||||
{
|
||||
uint edgeGuid = record.ServerGuid;
|
||||
hitGround = () =>
|
||||
{
|
||||
Console.WriteLine($"[remote-edge] guid={edgeGuid:X8} HitGround");
|
||||
rm.Movement.HitGround();
|
||||
};
|
||||
leaveGround = () =>
|
||||
{
|
||||
Console.WriteLine($"[remote-edge] guid={edgeGuid:X8} LeaveGround");
|
||||
rm.Motion.LeaveGround();
|
||||
};
|
||||
}
|
||||
if (!AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
rm.Body,
|
||||
resolved.InContact,
|
||||
|
|
@ -755,8 +775,8 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
|||
resolved.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable,
|
||||
rm.Movement.HitGround,
|
||||
rm.Motion.LeaveGround,
|
||||
hitGround,
|
||||
leaveGround,
|
||||
() => IsCurrentOwner(
|
||||
record,
|
||||
rm,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue