fix(physics): InterpolationManager review findings (L.3.1 Task 1 polish)
Addresses code-quality review findings on commit f43f168:
C-1: Stall detection re-implemented to match retail (acclient lines
353071-353275). Tracks _progressQuantum (sum of step values per window)
+ _distanceAtWindowStart (set at window start). Primary check:
cumulative_progress < MIN_DISTANCE_TO_REACH_POSITION (0.20m absolute).
Secondary check: cumulative_progress / _progressQuantum < 0.30.
Either failing increments fail counter; blip-to-tail at >3 consecutive
fails (already correct).
C-2: Renamed StallFailCountForBlip -> StallFailCountThreshold with
clearer XML doc explaining the > vs >= semantics (blip fires when fail
count EXCEEDS the threshold, i.e. on the 4th consecutive failed window).
I-1: _haveBaselineDistance sentinel prevents first-window false
positive that was triggering spurious fails on every new motion sequence
(old code defaulted _distanceAtWindowStart to 0, making cumulative
progress always negative on frame 5).
I-3: dt <= 0 || NaN guard at AdjustOffset entry prevents NaN
propagation into PhysicsBody.Position.
I-4: Internal field renames for clarity:
_failFrameCounter -> _framesSinceLastStallCheck
_failDistanceLastCheck -> merged into _distanceAtWindowStart
I-5: Added internal Count property + InternalsVisibleTo (via
AssemblyAttribute in .csproj) so Enqueue_DropsOldestWhenAtCap20
actually verifies cap enforcement. Added assertion that head is the
second-enqueued position after overflow.
3 new tests (AdjustOffset_FirstWindow_DoesNotFalseFail,
AdjustOffset_DtZeroOrNegative_ReturnsZero,
Enqueue_AtCap20_HeadIsSecondOriginal), 16 total. All green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f43f168916
commit
927636ec77
3 changed files with 213 additions and 56 deletions
|
|
@ -11,6 +11,11 @@
|
|||
<PackageReference Include="Chorizite.DatReaderWriter" Version="2.1.7" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>AcDream.Core.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace AcDream.Core.Physics;
|
|||
// guesses):
|
||||
// MAX_INTERPOLATED_VELOCITY_MOD = 2.0
|
||||
// MAX_INTERPOLATED_VELOCITY = 7.5
|
||||
// MIN_DISTANCE_TO_REACH_POSITION = 0.20 (unused here — kept for reference)
|
||||
// MIN_DISTANCE_TO_REACH_POSITION = 0.20 (absolute stall threshold, meters)
|
||||
// DESIRED_DISTANCE = 0.05
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -68,8 +68,8 @@ public sealed class InterpolationManager
|
|||
/// <summary>
|
||||
/// Per-5-frame stall progress threshold (meters). Body must advance at
|
||||
/// least this far in <see cref="StallCheckFrameInterval"/> frames or
|
||||
/// it counts as a stall tick.
|
||||
/// Retail MIN_DISTANCE_TO_REACH_POSITION (@ 0x00555D30).
|
||||
/// the window counts as a stall.
|
||||
/// Retail MIN_DISTANCE_TO_REACH_POSITION (@ 0x00555E42).
|
||||
/// </summary>
|
||||
public const float MinDistanceToReachPosition = 0.20f;
|
||||
|
||||
|
|
@ -83,38 +83,69 @@ public sealed class InterpolationManager
|
|||
|
||||
/// <summary>
|
||||
/// Number of ticks between stall progress checks.
|
||||
/// Retail StallCheckFrameInterval (@ 0x00555D30).
|
||||
/// Retail frame_counter threshold (@ 0x00555E14).
|
||||
/// </summary>
|
||||
public const int StallCheckFrameInterval = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum fraction of the expected advance that counts as "real
|
||||
/// progress" in a stall check window. Below this fraction the
|
||||
/// fail counter increments.
|
||||
/// Retail StallProgressMinFraction (@ 0x00555D30).
|
||||
/// Minimum fraction of cumulative progress_quantum that counts as "real
|
||||
/// progress" in a stall check window. Below this fraction the window
|
||||
/// counts as a stall (secondary check, applies when progress_quantum > 0).
|
||||
/// Retail CREATURE_FAILED_INTERPOLATION_PERCENTAGE (@ 0x00555E73).
|
||||
/// </summary>
|
||||
public const float StallProgressMinFraction = 0.30f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of consecutive stall-check failures before the body is
|
||||
/// blipped to the tail of the queue.
|
||||
/// Retail StallFailCountForBlip (@ 0x00555D30).
|
||||
/// Stall-fail counter threshold. The body is blipped to the tail of the
|
||||
/// queue when <c>node_fail_counter</c> EXCEEDS this value (i.e., on the
|
||||
/// 4th consecutive failed window, not the 3rd).
|
||||
/// Retail: <c>node_fail_counter > 3</c> (@ 0x00555F39).
|
||||
/// </summary>
|
||||
public const int StallFailCountForBlip = 3;
|
||||
public const int StallFailCountThreshold = 3;
|
||||
|
||||
// ── internals ─────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly LinkedList<InterpolationNode> _queue = new();
|
||||
|
||||
private int _failFrameCounter = 0;
|
||||
private float _failDistanceLastCheck = 0f;
|
||||
private int _failCount = 0;
|
||||
/// <summary>Frames elapsed since the last 5-frame stall-check window fired.</summary>
|
||||
private int _framesSinceLastStallCheck = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative sum of per-frame <c>step</c> magnitudes within the current
|
||||
/// 5-frame window. Retail <c>progress_quantum</c>.
|
||||
/// </summary>
|
||||
private float _progressQuantum = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Distance to the head node recorded at the START of the current
|
||||
/// 5-frame window. Retail <c>original_distance</c>.
|
||||
/// </summary>
|
||||
private float _distanceAtWindowStart = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// True once the first valid distance sample has been taken and
|
||||
/// <c>_distanceAtWindowStart</c> is populated. Guards against the
|
||||
/// first-window false-positive that occurs when the field defaults to 0.
|
||||
/// </summary>
|
||||
private bool _haveBaselineDistance = false;
|
||||
|
||||
/// <summary>
|
||||
/// Number of consecutive 5-frame windows that failed both the absolute
|
||||
/// and ratio progress checks. Retail <c>node_fail_counter</c>.
|
||||
/// Blip fires when this EXCEEDS <see cref="StallFailCountThreshold"/>.
|
||||
/// </summary>
|
||||
private int _failCount = 0;
|
||||
|
||||
// ── public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the queue holds at least one waypoint.</summary>
|
||||
public bool IsActive => _queue.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current waypoint count (visible to the test assembly for cap verification).
|
||||
/// </summary>
|
||||
internal int Count => _queue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Stop interpolating: clear the queue and reset all stall counters.
|
||||
/// Retail StopInterpolating / destructor cleanup.
|
||||
|
|
@ -122,9 +153,11 @@ public sealed class InterpolationManager
|
|||
public void Clear()
|
||||
{
|
||||
_queue.Clear();
|
||||
_failFrameCounter = 0;
|
||||
_failDistanceLastCheck = 0f;
|
||||
_failCount = 0;
|
||||
_framesSinceLastStallCheck = 0;
|
||||
_progressQuantum = 0f;
|
||||
_distanceAtWindowStart = 0f;
|
||||
_haveBaselineDistance = false;
|
||||
_failCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -172,8 +205,9 @@ public sealed class InterpolationManager
|
|||
/// <para>
|
||||
/// Returns <see cref="Vector3.Zero"/> when the queue is empty or when
|
||||
/// the head node has been reached. Returns a snap delta (tail −
|
||||
/// currentBodyPosition) after <see cref="StallFailCountForBlip"/>
|
||||
/// consecutive stall failures, then clears the queue.
|
||||
/// currentBodyPosition) after <see cref="StallFailCountThreshold"/>
|
||||
/// consecutive stall failures (i.e., fail count EXCEEDS the threshold),
|
||||
/// then clears the queue.
|
||||
/// </para>
|
||||
///
|
||||
/// Retail InterpolationManager::adjust_offset (@ 0x00555D30) +
|
||||
|
|
@ -189,6 +223,9 @@ public sealed class InterpolationManager
|
|||
/// <returns>World-space delta to apply to the body this frame.</returns>
|
||||
public Vector3 AdjustOffset(double dt, Vector3 currentBodyPosition, float maxSpeedFromMinterp)
|
||||
{
|
||||
// Guard: bad dt → skip entirely to prevent NaN poisoning PhysicsBody.Position.
|
||||
if (dt <= 0 || double.IsNaN(dt)) return Vector3.Zero;
|
||||
|
||||
// Step 1: empty queue → no correction
|
||||
if (_queue.First is null)
|
||||
return Vector3.Zero;
|
||||
|
|
@ -218,23 +255,58 @@ public sealed class InterpolationManager
|
|||
// Step 7: direction × step
|
||||
Vector3 delta = ((headNode.TargetPosition - currentBodyPosition) / dist) * step;
|
||||
|
||||
// Step 8: stall detection
|
||||
_failFrameCounter++;
|
||||
if (_failFrameCounter >= StallCheckFrameInterval)
|
||||
{
|
||||
float progress = _failDistanceLastCheck - dist;
|
||||
float expected = catchUpSpeed * (float)dt * StallCheckFrameInterval;
|
||||
// Step 8: stall detection (retail adjust_offset @ 0x00555E08-0x00555E92)
|
||||
//
|
||||
// Retail tracks two quantities across each 5-frame window:
|
||||
// progress_quantum — cumulative sum of per-frame step magnitudes
|
||||
// original_distance — distance to head at the START of the window
|
||||
//
|
||||
// At window end (frame_counter >= 5):
|
||||
// cumulative_progress = original_distance - currentDist
|
||||
//
|
||||
// Primary check (@ 0x00555E42):
|
||||
// cumulative_progress < MIN_DISTANCE_TO_REACH_POSITION (0.20 m)
|
||||
// → window is a stall; increment node_fail_counter.
|
||||
//
|
||||
// Secondary check (@ 0x00555E73, only when progress_quantum > 0):
|
||||
// cumulative_progress / progress_quantum < CREATURE_FAILED_INTERPOLATION_PERCENTAGE (0.30)
|
||||
// → window is a stall; increment node_fail_counter.
|
||||
//
|
||||
// Both checks operate with sticky_object_id == 0 (we never have one).
|
||||
// Either check failing counts the window as a stall.
|
||||
//
|
||||
// Blip fires when node_fail_counter > 3 (retail UseTime @ 0x00555F39).
|
||||
// Window always resets (frame_counter=0, progress_quantum=0,
|
||||
// original_distance=currentDist) after the check.
|
||||
|
||||
if (progress < StallProgressMinFraction * expected)
|
||||
// Initialise window baseline on first call after Clear / new motion.
|
||||
if (!_haveBaselineDistance)
|
||||
{
|
||||
_distanceAtWindowStart = dist;
|
||||
_haveBaselineDistance = true;
|
||||
}
|
||||
|
||||
_progressQuantum += step;
|
||||
_framesSinceLastStallCheck++;
|
||||
|
||||
if (_framesSinceLastStallCheck >= StallCheckFrameInterval)
|
||||
{
|
||||
float cumulativeProgress = _distanceAtWindowStart - dist;
|
||||
|
||||
bool primaryFail = cumulativeProgress < MinDistanceToReachPosition;
|
||||
bool secondaryFail = _progressQuantum > 0f &&
|
||||
(cumulativeProgress / _progressQuantum) < StallProgressMinFraction;
|
||||
|
||||
if (primaryFail || secondaryFail)
|
||||
{
|
||||
_failCount++;
|
||||
if (_failCount > StallFailCountForBlip)
|
||||
// Blip-to-tail: retail UseTime (@ 0x00555F20) reads
|
||||
// position_queue.tail_, copies its position to a local,
|
||||
// calls CPhysicsObj::SetPositionSimple, then
|
||||
// StopInterpolating. Snap target is the TAIL (the most
|
||||
// recent server position), not the head.
|
||||
if (_failCount > StallFailCountThreshold)
|
||||
{
|
||||
// Blip-to-tail: retail UseTime (@ 0x00555F20) reads
|
||||
// position_queue.tail_, copies its position to a local,
|
||||
// calls CPhysicsObj::SetPositionSimple, then
|
||||
// StopInterpolating. Snap target is the TAIL (the most
|
||||
// recent server position), not the head.
|
||||
Vector3 tailPos = _queue.Last!.Value.TargetPosition;
|
||||
Clear();
|
||||
return tailPos - currentBodyPosition;
|
||||
|
|
@ -245,8 +317,10 @@ public sealed class InterpolationManager
|
|||
_failCount = 0;
|
||||
}
|
||||
|
||||
_failDistanceLastCheck = dist;
|
||||
_failFrameCounter = 0;
|
||||
// Reset the 5-frame window regardless of pass/fail.
|
||||
_framesSinceLastStallCheck = 0;
|
||||
_progressQuantum = 0f;
|
||||
_distanceAtWindowStart = dist;
|
||||
}
|
||||
|
||||
// Step 9: return per-frame delta
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue