fix(physics): port retail slope landing stop
This commit is contained in:
parent
1d8371dbe5
commit
5a0f9868a6
13 changed files with 870 additions and 103 deletions
|
|
@ -142,7 +142,7 @@ remain in place (ride ACDREAM_DUMP_MOTION=1) until the user gate passes.
|
|||
|
||||
## #269 — Slope-stop slide runs too far (post-bounce-rework residual)
|
||||
|
||||
**Status:** OPEN (user live gate 2026-07-30 — "almost pass with merits")
|
||||
**Status:** DONE — 2026-07-31 (implementation + user live gate)
|
||||
**Severity:** LOW-MEDIUM (feel residual; bounce family otherwise accepted)
|
||||
**Component:** physics / landing slide decay
|
||||
|
||||
|
|
@ -162,14 +162,24 @@ cos(10°) with base 0.2); the jump chain end-to-end (`GetJumpHeight`
|
|||
Sledding auto-toggle (P2 finding re-confirmed; no `state |= 0x800000`
|
||||
writer exists).
|
||||
|
||||
**Next step (behavior question, not code):** live A/B per the cdb
|
||||
workflow — breakpoints on `calc_friction`/`handle_all_collisions` dumping
|
||||
velocity per tick while the retail client lands + slides on a slope, vs an
|
||||
`ACDREAM_CAPTURE_RESOLVE` capture of the same maneuver on the same slope.
|
||||
Compare the decay curves; the first diverging tick names the mechanism.
|
||||
Candidates the trace discriminates: hop-chain cadence (friction skips
|
||||
airborne micro-hop ticks), gravity-tangential rebuild timing, quantum
|
||||
alignment.
|
||||
**Resolution (2026-07-31):** a 2,184-quantum live capture isolated the
|
||||
first divergence. The landing tick produced retail's correct 5% reflect,
|
||||
but the following quanta repeatedly restored
|
||||
`LastKnownContactPlane` while retaining the reflected velocity. The body
|
||||
therefore remained Contact + OnWalkable with `v·n > 0.25`, where retail
|
||||
`calc_friction` intentionally does no work, and slid at full speed.
|
||||
|
||||
Named-retail `CTransition::validate_transition @ 0x0050AA70` revealed the
|
||||
omission: its non-OK remembered-plane recovery calls
|
||||
`OBJECTINFO::kill_velocity @ 0x0050CFE0` *before* the proximity test and
|
||||
plane restore (`0x0050AAED–0x0050AB42`). It also consumes the remembered
|
||||
plane only in that non-OK branch and overwrites last-known validity from
|
||||
the final contact plane at `0x0050ACFF`. ACDream now follows that exact
|
||||
ordering. Focused collision-recovery and clean-advance pins pass; full
|
||||
Core (4,107/2 skips) and Runtime (439/0) suites pass; the user repeated the
|
||||
slope-jump test and accepted the result (“Perfect! Works great!”).
|
||||
Capture and decode:
|
||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -46,12 +46,14 @@ a ledger pass. Goal: zero physics TS rows, no unargued feel-affecting AP
|
|||
rows, one final batched connected visual matrix. Sonnet implements, Opus
|
||||
reviews. The plan is
|
||||
[`2026-07-29-physics-parity-campaign.md`](2026-07-29-physics-parity-campaign.md).
|
||||
The 2026-07-31 #268 stat-chain package is implemented pending its live visual
|
||||
gate: panel and Runtime movement share retail's complete augmentation ordering,
|
||||
The 2026-07-31 #268 stat-chain package is implemented and user-accepted:
|
||||
panel and Runtime movement share retail's complete augmentation ordering,
|
||||
the authored per-fragment vitae/buff/debuff colors are live, and AP-127 plus
|
||||
TS-8 are retired by focused and end-to-end packet tests. The remaining
|
||||
implementation target is #269's capture-driven slope-slide feel residual,
|
||||
followed by the unfinished live matrix rows.
|
||||
TS-8 are retired by focused and end-to-end packet tests. #269's
|
||||
capture-driven slope-slide residual is also closed and user-accepted:
|
||||
`CTransition::validate_transition` now performs retail's non-OK-only
|
||||
remembered-plane restore with the preceding `OBJECTINFO::kill_velocity`.
|
||||
Campaign P now continues with the unfinished live matrix rows.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -380,13 +380,19 @@ root-caused, retail-ported, and user-accepted in the same session:
|
|||
authored 0x1B palette exactly: #7FFFFF vitae, #00FF00 buff, #FF0000
|
||||
debuff. TS-8 is also retired: a real live 0x02C2 payload carries its full
|
||||
StatMod through dispatch and changes the effective skill immediately.
|
||||
- **#269 filed** — slope-stop slide sometimes runs long. `calc_friction`
|
||||
(0x0050ee70) and the complete jump chain (`GetJumpHeight` 0x006b09b0,
|
||||
`InqJumpVelocity` vz=sqrt(h·19.6), 1.0 s/0.8 s powerbar charge) are
|
||||
byte-verified identical — the user's jump-height hypothesis is
|
||||
refuted; next step is the live cdb A/B decay-curve trace.
|
||||
- **#269 closed 2026-07-31** — the live 2,184-quantum trace proved the
|
||||
landing reflect and friction math were correct. ACDream omitted retail's
|
||||
`OBJECTINFO::kill_velocity` before restoring a remembered contact plane
|
||||
in `CTransition::validate_transition @ 0x0050AA70`, retaining full
|
||||
downhill velocity while repeatedly re-grounding the mover. The exact
|
||||
non-OK-only restore/kill order and final last-known validity overwrite
|
||||
are now ported, focused/full gates pass, and the user accepted repeated
|
||||
slope jumps. Evidence:
|
||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
||||
|
||||
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
||||
flat pop, uphill landing (rows 3/4/5-partial/12-partial). Remaining
|
||||
rows: 1, 2, 6, 7, 8, 9, 10, 11 plus #269's slide feel. Suite at this
|
||||
checkpoint: 10,031 passed / 5 skips / 0 failures.
|
||||
flat pop, uphill landing, and #269's slope-stop feel
|
||||
(rows 3/4/5-partial/12-partial). Remaining rows: 1, 2, 6, 7, 8, 9, 10,
|
||||
and 11. The #269 checkpoint passes 4,107 Core tests / 2 skips and 439
|
||||
Runtime tests / 0 skips; the complete Release suite passes 10,061 tests /
|
||||
5 skips / 0 failures.
|
||||
|
|
|
|||
104
docs/research/2026-07-31-269-slope-stop-capture.md
Normal file
104
docs/research/2026-07-31-269-slope-stop-capture.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Issue #269 — slope-stop capture and retail correction
|
||||
|
||||
**Date:** 2026-07-31
|
||||
**Status:** implemented; user live gate passed
|
||||
**Scope:** landing-bounce follow-up, `CTransition::validate_transition`
|
||||
|
||||
## Symptom
|
||||
|
||||
After the retail 5%-elasticity landing reflection was restored for #265,
|
||||
the character could retain too much downhill speed after landing on a
|
||||
walkable slope. The user described the residual as “slides too far on
|
||||
landing.”
|
||||
|
||||
## ACDream live capture
|
||||
|
||||
`ACDREAM_CAPTURE_PLAYER_QUANTA=<jsonl-path>` records the local player's
|
||||
complete admitted object quantum without changing simulation order:
|
||||
|
||||
1. quantum start;
|
||||
2. root/PositionManager composition;
|
||||
3. pre- and post-`UpdatePhysicsInternal`;
|
||||
4. transition result;
|
||||
5. final collision-response commit.
|
||||
|
||||
The accepted repro contained 2,184 quanta. The clearest landing was:
|
||||
|
||||
| Quantum | Event | Velocity |
|
||||
|---|---|---|
|
||||
| 1740 | final airborne quantum | `(-12.316, 8.187, -26.266)` |
|
||||
| 1741 | slope collision, normal `(-0.236, 0.236, 0.943)` | |
|
||||
| 1741 post-response | correct 5% reflect | `(-17.391, 13.262, -6.576)` |
|
||||
| 1742–1758 | still Contact + OnWalkable, no new collision normal | velocity unchanged |
|
||||
| 1759+ | contact relationship changes | friction finally begins decaying |
|
||||
|
||||
The reflected velocity had `dot(v, normal) = +1.0252`: it pointed away
|
||||
from the slope. Retail `calc_friction` correctly skips while this value is
|
||||
at least `0.25`, so friction was not the defect. ACDream was repeatedly
|
||||
restoring the remembered slope plane and re-grounding the body without
|
||||
performing retail's accompanying velocity stop.
|
||||
|
||||
## Retail oracle
|
||||
|
||||
Named-retail:
|
||||
|
||||
- `CPhysicsObj::check_contact` `0x0050F5B0`
|
||||
- `CPhysicsObj::get_object_info` `0x00511CC0`
|
||||
- `CTransition::validate_transition` `0x0050AA70`
|
||||
- `OBJECTINFO::kill_velocity` `0x0050CFE0`
|
||||
|
||||
The exact `validate_transition` order at
|
||||
`0x0050AAED–0x0050AB42` is:
|
||||
|
||||
1. enter only for a non-OK collision/adjusted/slid result;
|
||||
2. if `last_known_contact_plane_valid`, call
|
||||
`OBJECTINFO::kill_velocity`;
|
||||
3. test the current sphere center against the remembered plane using
|
||||
`radius + 0.0002`;
|
||||
4. restore the contact plane only when still within that distance;
|
||||
5. later, at `0x0050ACFF`, overwrite last-known validity with final
|
||||
contact-plane validity.
|
||||
|
||||
`OBJECTINFO::kill_velocity` calls
|
||||
`CPhysicsObj::set_velocity({0,0,0}, 0)`. ACDream had ported the proximity
|
||||
test and plane restore but omitted this call. It also allowed the
|
||||
last-known plane to re-ground clean accepted moves, although retail only
|
||||
consumes it in the non-OK recovery branch.
|
||||
|
||||
## Correction
|
||||
|
||||
`Transition.ValidateTransition` now:
|
||||
|
||||
- calls `ObjectInfo.StopVelocity()` before the remembered-plane
|
||||
proximity/restore test on a non-OK recovery;
|
||||
- performs that restore only in the retail branch;
|
||||
- overwrites last-known validity from final contact validity, so a clean
|
||||
move away cannot be re-grounded from stale memory.
|
||||
|
||||
The existing `PhysicsEngine.ResolveWithTransition` consumption of
|
||||
`VelocityKilled` applies the zero to the canonical `PhysicsBody` before
|
||||
the collision-response tail. The initial 5% landing reflection remains;
|
||||
only a following collision recovery performs the retail stop.
|
||||
|
||||
## Gates
|
||||
|
||||
- New focused pins:
|
||||
- collision recovery with a remembered plane kills velocity;
|
||||
- clean advance with a remembered plane neither kills nor re-grounds.
|
||||
- Full `AcDream.Core.Tests`: 4,107 passed / 2 skipped.
|
||||
- Full `AcDream.Runtime.Tests`: 439 passed.
|
||||
- `AcDream.App` Release build: 0 warnings / 0 errors.
|
||||
- Complete Release suite: 10,061 passed / 5 skipped / 0 failed.
|
||||
- User live gate: **PASS** — repeated slope jumps now settle correctly
|
||||
(“Perfect! Works great!”).
|
||||
|
||||
## Diagnostic tools retained
|
||||
|
||||
- `tools/analyze_269_slope_stop_capture.py`
|
||||
- `tools/cdb/run-issue269-slope-stop.ps1`
|
||||
- `tools/cdb/issue269-slope-stop.cdb`
|
||||
|
||||
The cdb runner refuses to attach unless the live retail executable matches
|
||||
the Sept 2013 named PDB. The locally installed 2015 retail executable does
|
||||
not match; the static named-retail decode above is therefore the retail
|
||||
oracle used for this correction.
|
||||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
## Phase
|
||||
|
||||
Active phase: **L.2 - Movement & Collision Conformance**.
|
||||
Active phase: **Campaign P — Physics Retail-Feel Parity** (the L.2/R6
|
||||
foundation is shipped).
|
||||
|
||||
Plan: `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||||
Plan: `docs/plans/2026-07-29-physics-parity-campaign.md`.
|
||||
|
||||
Roadmap: `docs/plans/2026-04-11-roadmap.md`.
|
||||
|
||||
|
|
@ -78,3 +79,11 @@ InputDispatcher / PlayerMovementController
|
|||
re-test the adjusted `CheckPos`), not returned to `ValidateTransition`; the
|
||||
outer validator treats non-OK as a collision and restores `CurPos`, making
|
||||
edges feel like hard stops even when the tangent was computed.
|
||||
- 2026-07-31: Campaign P #269 slope-stop parity. A 2,184-quantum live
|
||||
capture proved the 5% landing reflect and `calc_friction` were correct,
|
||||
but `ValidateTransition` restored `LastKnownContactPlane` without retail's
|
||||
preceding `OBJECTINFO::kill_velocity`. Retail consumes that plane only on
|
||||
a non-OK recovery and overwrites last-known validity from the final contact
|
||||
plane. Both rules are now ported; focused/full tests and the user's repeated
|
||||
slope-jump gate pass. See
|
||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
||||
|
|
|
|||
|
|
@ -5530,6 +5530,30 @@ public sealed class Transition
|
|||
else if (transitionState != TransitionState.Invalid)
|
||||
{
|
||||
// Collision/slide/adjusted: revert to current position.
|
||||
// Retail CTransition::validate_transition 0x0050AA70 consumes a
|
||||
// remembered contact plane only on this non-OK recovery path. It
|
||||
// first calls OBJECTINFO::kill_velocity, then restores the plane
|
||||
// when the current sphere remains within radius + EPSILON. Omitting
|
||||
// the kill preserved a landing reflection while repeatedly
|
||||
// re-grounding the mover, producing Campaign P #269's long slide.
|
||||
if (ci.LastKnownContactPlaneValid)
|
||||
{
|
||||
oi.StopVelocity();
|
||||
|
||||
var sphereCenter = sp.GlobalCurrCenter[0].Origin;
|
||||
var radius = sp.GlobalSphere[0].Radius;
|
||||
float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter)
|
||||
+ ci.LastKnownContactPlane.D;
|
||||
|
||||
if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle))
|
||||
{
|
||||
ci.SetContactPlane(
|
||||
ci.LastKnownContactPlane,
|
||||
ci.LastKnownContactPlaneCellId,
|
||||
ci.LastKnownContactPlaneIsWater);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ci.CollisionNormalValid)
|
||||
ci.SetCollisionNormal(Vector3.UnitZ); // default: push up
|
||||
|
||||
|
|
@ -5541,14 +5565,11 @@ public sealed class Transition
|
|||
if (ci.CollisionNormalValid)
|
||||
ci.SetSlidingNormal(ci.CollisionNormal);
|
||||
|
||||
// Preserve contact plane for next step.
|
||||
// L.2.3c (2026-04-29): only OVERWRITE LastKnown when current is valid.
|
||||
// Previously: `LastKnownValid = ContactPlaneValid` cleared
|
||||
// LastKnown whenever current was invalid — destroying the prior frame's
|
||||
// contact memory. After StepUpSlide cleared ContactPlane mid-step
|
||||
// (failed step-up against a too-tall wall), this propagated to
|
||||
// LastKnown and the player went airborne for a frame, flickering the
|
||||
// falling animation. Now LastKnown survives transient losses.
|
||||
// Retail 0x0050ACFF-0x0050AD9A overwrites last-known validity with
|
||||
// current contact validity, copies the plane only when valid, and then
|
||||
// derives Contact/OnWalkable from that final plane. A remembered plane
|
||||
// can have been restored only in the non-OK recovery branch above; it
|
||||
// must never re-ground a clean move away from the surface.
|
||||
if (ci.ContactPlaneValid)
|
||||
{
|
||||
ci.LastKnownContactPlaneValid = true;
|
||||
|
|
@ -5569,79 +5590,9 @@ public sealed class Transition
|
|||
else
|
||||
oi.State &= ~ObjectInfoState.OnWalkable;
|
||||
}
|
||||
else if (ci.LastKnownContactPlaneValid)
|
||||
{
|
||||
// L.2.3c: current contact lost transiently (e.g. StepUpSlide
|
||||
// cleared it during a failed step-up) but the prior frame's
|
||||
// contact is still valid — keep the mover grounded via the
|
||||
// last-known plane. Without this, every wall bump dropped the
|
||||
// player into the falling animation for one frame.
|
||||
//
|
||||
// L.2.4 (2026-04-30): PROXIMITY GUARD. Only trust the
|
||||
// last-known plane if the sphere is still actually near it.
|
||||
// Geometrically: `angle` is the signed distance from the
|
||||
// sphere center to the plane. If |angle| exceeds the sphere
|
||||
// radius (plus a tiny epsilon), the sphere has SEPARATED
|
||||
// from the plane — typically because we fell off an edge or
|
||||
// the body dropped vertically while the resolver bounced
|
||||
// through edge-slide attempts. Without this guard the player
|
||||
// gets stuck mid-fall in a falling animation forever (live
|
||||
// bug 2026-04-30: cur.Z=96.6, check.Z=95.1 — 1.5 m below the
|
||||
// remembered floor, but still being marked Contact + OnWalkable).
|
||||
//
|
||||
// Matches ACE PhysicsObj's pre-reuse check on the last-known
|
||||
// plane and retail's CPhysicsObj::get_object_info logic.
|
||||
// A6.P3 slice 1 (2026-05-21). Retail uses global_curr_center (NOT
|
||||
// global_sphere->center) for this proximity check — see
|
||||
// acclient_2013_pseudo_c.txt:272568. global_sphere is the START
|
||||
// sphere of the transition; global_curr_center is the CURRENT center
|
||||
// after sub-step accumulation. Using the wrong one made the proximity
|
||||
// guard fire on the wrong reference point.
|
||||
var sphereCenter = sp.GlobalCurrCenter[0].Origin;
|
||||
var radius = sp.GlobalSphere[0].Radius;
|
||||
float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter)
|
||||
+ ci.LastKnownContactPlane.D;
|
||||
|
||||
if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle))
|
||||
{
|
||||
// ── Mechanism B — restore CP from LKCP per retail ────────────────
|
||||
// A6.P3 slice 1 (2026-05-21). Retail oracle:
|
||||
// acclient_2013_pseudo_c.txt:272577 (inside CTransition::validate_transition
|
||||
// at line 272547). When the sphere is geometrically close to the
|
||||
// LastKnownContactPlane, retail restores CP from LKCP via
|
||||
// set_contact_plane(&collision_info, &last_known_contact_plane,
|
||||
// last_known_contact_plane_is_water). This closes the gap that the
|
||||
// stripped TryFindIndoorWalkablePlane synthesis path used to fill —
|
||||
// when no fresh Path-6 CP write lands in this transition, CP is
|
||||
// retained from the previous frame instead of being re-synthesized.
|
||||
//
|
||||
// NOTE: SetContactPlane also re-latches LKCP fields
|
||||
// (TransitionTypes.cs:258-261), which is a no-op here since we
|
||||
// pass LKCP as the source.
|
||||
ci.SetContactPlane(
|
||||
ci.LastKnownContactPlane,
|
||||
ci.LastKnownContactPlaneCellId,
|
||||
ci.LastKnownContactPlaneIsWater);
|
||||
|
||||
// Still close enough to the last-known plane — preserve
|
||||
// grounded state. L.2.3i FloorZ test for OnWalkable.
|
||||
oi.State |= ObjectInfoState.Contact;
|
||||
if (ci.LastKnownContactPlane.Normal.Z >= PhysicsGlobals.FloorZ)
|
||||
oi.State |= ObjectInfoState.OnWalkable;
|
||||
else
|
||||
oi.State &= ~ObjectInfoState.OnWalkable;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sphere has separated from the last-known plane.
|
||||
// Drop the memory and let the body resolve normally
|
||||
// (gravity → next-frame terrain probe → real contact).
|
||||
ci.LastKnownContactPlaneValid = false;
|
||||
oi.State &= ~(ObjectInfoState.Contact | ObjectInfoState.OnWalkable);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ci.LastKnownContactPlaneValid = false;
|
||||
oi.State &= ~(ObjectInfoState.Contact | ObjectInfoState.OnWalkable);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1841,6 +1841,11 @@ public sealed class PlayerMovementController
|
|||
for (int qi = 0; qi < quantumBatch.Count; qi++)
|
||||
{
|
||||
float tickDt = quantumBatch.GetQuantum(qi);
|
||||
bool captureQuantum = PlayerPhysicsQuantumCapture.IsEnabled;
|
||||
uint captureCellBefore = CellId;
|
||||
PlayerPhysicsBodyTraceSnapshot captureQuantumStart = captureQuantum
|
||||
? PlayerPhysicsQuantumCapture.Snapshot(_body)
|
||||
: default;
|
||||
|
||||
// CPhysicsObj::UpdatePositionInternal (0x00512C30): visible objects
|
||||
// advance their PartArray first. The complete Frame survives; only its
|
||||
|
|
@ -1931,7 +1936,13 @@ public sealed class PlayerMovementController
|
|||
}
|
||||
|
||||
_body.calc_acceleration();
|
||||
PlayerPhysicsBodyTraceSnapshot capturePreIntegration = captureQuantum
|
||||
? PlayerPhysicsQuantumCapture.Snapshot(_body)
|
||||
: default;
|
||||
_body.UpdatePhysicsInternal(tickDt);
|
||||
PlayerPhysicsBodyTraceSnapshot capturePostIntegration = captureQuantum
|
||||
? PlayerPhysicsQuantumCapture.Snapshot(_body)
|
||||
: default;
|
||||
|
||||
// Retail process_hooks is the final UpdatePositionInternal step:
|
||||
// after physics, before the transition and manager tail.
|
||||
|
|
@ -2121,6 +2132,30 @@ public sealed class PlayerMovementController
|
|||
_motion.CheckForCompletedMotions,
|
||||
PositionManager);
|
||||
|
||||
if (captureQuantum)
|
||||
{
|
||||
PlayerPhysicsQuantumCapture.Log(
|
||||
tickDt,
|
||||
captureCellBefore,
|
||||
CellId,
|
||||
input,
|
||||
capturePreIntegration.Position - captureQuantumStart.Position,
|
||||
candidateMoved,
|
||||
captureQuantumStart,
|
||||
capturePreIntegration,
|
||||
capturePostIntegration,
|
||||
new PlayerPhysicsResolveTraceSnapshot(
|
||||
resolveResult.Position,
|
||||
resolveResult.CellId,
|
||||
resolveResult.Ok,
|
||||
resolveResult.IsOnGround,
|
||||
resolveResult.InContact,
|
||||
resolveResult.OnWalkable,
|
||||
resolveResult.CollisionNormalValid,
|
||||
resolveResult.CollisionNormal),
|
||||
PlayerPhysicsQuantumCapture.Snapshot(_body));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── 4. Determine outbound motion commands ─────────────────────────────
|
||||
|
|
|
|||
191
src/AcDream.Runtime/Gameplay/PlayerPhysicsQuantumCapture.cs
Normal file
191
src/AcDream.Runtime/Gameplay/PlayerPhysicsQuantumCapture.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in, player-only trace of the complete retail object quantum.
|
||||
/// <para>
|
||||
/// Set <c>ACDREAM_CAPTURE_PLAYER_QUANTA=<path></c> before process
|
||||
/// startup to emit one JSON-Lines record per admitted physics quantum. The
|
||||
/// capture is deliberately outside the physics implementation: it observes
|
||||
/// the same stage boundaries as retail
|
||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> without changing their order or
|
||||
/// introducing a second simulation path. When disabled, the hot path pays
|
||||
/// one static null/empty check and performs no allocations.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class PlayerPhysicsQuantumCapture
|
||||
{
|
||||
internal static string? CapturePath { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_CAPTURE_PLAYER_QUANTA");
|
||||
|
||||
internal static bool IsEnabled => !string.IsNullOrWhiteSpace(CapturePath);
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
{
|
||||
IncludeFields = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
};
|
||||
|
||||
private static readonly object s_writerLock = new();
|
||||
private static StreamWriter? s_writer;
|
||||
private static long s_sequence;
|
||||
private static bool s_processExitHooked;
|
||||
|
||||
internal static PlayerPhysicsBodyTraceSnapshot Snapshot(PhysicsBody body) =>
|
||||
new(
|
||||
Position: body.Position,
|
||||
Orientation: body.Orientation,
|
||||
Velocity: body.Velocity,
|
||||
Acceleration: body.Acceleration,
|
||||
GroundNormal: body.GroundNormal,
|
||||
ContactPlaneValid: body.ContactPlaneValid,
|
||||
ContactPlane: body.ContactPlane,
|
||||
Friction: body.Friction,
|
||||
Elasticity: body.Elasticity,
|
||||
State: (uint)body.State,
|
||||
TransientState: (uint)body.TransientState,
|
||||
FramesStationaryFall: body.FramesStationaryFall);
|
||||
|
||||
internal static void Log(
|
||||
float dt,
|
||||
uint cellBefore,
|
||||
uint cellAfter,
|
||||
MovementInput input,
|
||||
Vector3 rootAndManagerDelta,
|
||||
bool candidateMoved,
|
||||
PlayerPhysicsBodyTraceSnapshot quantumStart,
|
||||
PlayerPhysicsBodyTraceSnapshot preIntegration,
|
||||
PlayerPhysicsBodyTraceSnapshot postIntegration,
|
||||
PlayerPhysicsResolveTraceSnapshot resolve,
|
||||
PlayerPhysicsBodyTraceSnapshot postCommit)
|
||||
{
|
||||
string? path = CapturePath;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
|
||||
var record = new PlayerPhysicsQuantumTraceRecord(
|
||||
Sequence: Interlocked.Increment(ref s_sequence) - 1,
|
||||
TimestampTicks: System.Diagnostics.Stopwatch.GetTimestamp(),
|
||||
Dt: dt,
|
||||
CellBefore: cellBefore,
|
||||
CellAfter: cellAfter,
|
||||
Input: new PlayerMovementInputTraceSnapshot(
|
||||
input.Forward,
|
||||
input.Backward,
|
||||
input.StrafeLeft,
|
||||
input.StrafeRight,
|
||||
input.TurnLeft,
|
||||
input.TurnRight,
|
||||
input.Run,
|
||||
input.Jump),
|
||||
RootAndManagerDelta: rootAndManagerDelta,
|
||||
CandidateMoved: candidateMoved,
|
||||
QuantumStart: quantumStart,
|
||||
PreIntegration: preIntegration,
|
||||
PostIntegration: postIntegration,
|
||||
Resolve: resolve,
|
||||
PostCommit: postCommit);
|
||||
|
||||
string json = JsonSerializer.Serialize(record, s_jsonOptions);
|
||||
lock (s_writerLock)
|
||||
{
|
||||
EnsureWriter_NoLock(path);
|
||||
s_writer!.WriteLine(json);
|
||||
s_writer.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Close()
|
||||
{
|
||||
lock (s_writerLock)
|
||||
{
|
||||
s_writer?.Dispose();
|
||||
s_writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTest()
|
||||
{
|
||||
Close();
|
||||
CapturePath = null;
|
||||
Interlocked.Exchange(ref s_sequence, 0);
|
||||
}
|
||||
|
||||
private static void EnsureWriter_NoLock(string path)
|
||||
{
|
||||
if (s_writer is not null)
|
||||
return;
|
||||
|
||||
string? directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
s_writer = new StreamWriter(new FileStream(
|
||||
path,
|
||||
FileMode.Append,
|
||||
FileAccess.Write,
|
||||
FileShare.Read))
|
||||
{
|
||||
AutoFlush = false,
|
||||
};
|
||||
|
||||
if (!s_processExitHooked)
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += static (_, _) => Close();
|
||||
s_processExitHooked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PlayerPhysicsQuantumTraceRecord(
|
||||
long Sequence,
|
||||
long TimestampTicks,
|
||||
float Dt,
|
||||
uint CellBefore,
|
||||
uint CellAfter,
|
||||
PlayerMovementInputTraceSnapshot Input,
|
||||
Vector3 RootAndManagerDelta,
|
||||
bool CandidateMoved,
|
||||
PlayerPhysicsBodyTraceSnapshot QuantumStart,
|
||||
PlayerPhysicsBodyTraceSnapshot PreIntegration,
|
||||
PlayerPhysicsBodyTraceSnapshot PostIntegration,
|
||||
PlayerPhysicsResolveTraceSnapshot Resolve,
|
||||
PlayerPhysicsBodyTraceSnapshot PostCommit);
|
||||
|
||||
internal readonly record struct PlayerMovementInputTraceSnapshot(
|
||||
bool Forward,
|
||||
bool Backward,
|
||||
bool StrafeLeft,
|
||||
bool StrafeRight,
|
||||
bool TurnLeft,
|
||||
bool TurnRight,
|
||||
bool Run,
|
||||
bool Jump);
|
||||
|
||||
internal readonly record struct PlayerPhysicsBodyTraceSnapshot(
|
||||
Vector3 Position,
|
||||
Quaternion Orientation,
|
||||
Vector3 Velocity,
|
||||
Vector3 Acceleration,
|
||||
Vector3 GroundNormal,
|
||||
bool ContactPlaneValid,
|
||||
Plane ContactPlane,
|
||||
float Friction,
|
||||
float Elasticity,
|
||||
uint State,
|
||||
uint TransientState,
|
||||
int FramesStationaryFall);
|
||||
|
||||
internal readonly record struct PlayerPhysicsResolveTraceSnapshot(
|
||||
Vector3 Position,
|
||||
uint CellId,
|
||||
bool Ok,
|
||||
bool IsOnGround,
|
||||
bool InContact,
|
||||
bool OnWalkable,
|
||||
bool CollisionNormalValid,
|
||||
Vector3 CollisionNormal);
|
||||
|
|
@ -483,6 +483,54 @@ public class BSPStepUpTests
|
|||
"wall (failed step-up should preserve LastKnownContactPlane).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P #269: retail validate_transition calls kill_velocity before
|
||||
/// restoring a last-known contact plane on a non-OK collision recovery.
|
||||
/// This is the post-landing stop mechanism when the 5% reflect points away
|
||||
/// from a slope but the next tangential sweep collides with it again.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void D1b_LastKnownPlaneCollisionRecovery_KillsVelocity()
|
||||
{
|
||||
var (root, resolved) = BSPStepUpFixtures.TallWall();
|
||||
var transition = BSPStepUpFixtures.MakeGroundedTransition(
|
||||
from: new Vector3(0.1f, 0f, 0f),
|
||||
to: new Vector3(0.6f, 0f, 0f),
|
||||
stepUpHeight: 0.04f);
|
||||
var engine = MakeTestEngine(root, resolved, terrainZ: 0f);
|
||||
|
||||
transition.FindTransitionalPosition(engine);
|
||||
|
||||
Assert.True(
|
||||
transition.ObjectInfo.VelocityKilled,
|
||||
"Retail OBJECTINFO::kill_velocity must run before restoring the " +
|
||||
"remembered floor after a collision/slide retry.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The same remembered plane must not kill velocity or re-ground a clean
|
||||
/// accepted move away from it. Retail consumes LastKnownContactPlane only
|
||||
/// inside validate_transition's non-OK recovery branch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void D1c_LastKnownPlaneCleanAdvance_DoesNotKillOrReground()
|
||||
{
|
||||
var (root, resolved) = BSPStepUpFixtures.TallWall();
|
||||
var transition = BSPStepUpFixtures.MakeAirborneTransition(
|
||||
from: new Vector3(-2f, 0f, 2f),
|
||||
to: new Vector3(-2f, 0f, 2.25f));
|
||||
transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, -1.52f);
|
||||
transition.CollisionInfo.LastKnownContactPlaneValid = true;
|
||||
var engine = MakeTestEngine(root, resolved, terrainZ: -50f);
|
||||
|
||||
transition.FindTransitionalPosition(engine);
|
||||
|
||||
Assert.False(transition.ObjectInfo.VelocityKilled);
|
||||
Assert.False(transition.CollisionInfo.ContactPlaneValid);
|
||||
Assert.False(transition.CollisionInfo.LastKnownContactPlaneValid);
|
||||
Assert.False(transition.ObjectInfo.OnWalkable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L.2.3b regression: Path 5 dispatch must be guarded against re-entry while
|
||||
/// a step-up is already in progress. Test runs <c>FindTransitionalPosition</c>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
[Collection(PlayerPhysicsQuantumCaptureCollection.Name)]
|
||||
public sealed class PlayerPhysicsQuantumCaptureTests : IDisposable
|
||||
{
|
||||
private readonly string _path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"acdream-player-physics-{Guid.NewGuid():N}.jsonl");
|
||||
|
||||
public PlayerPhysicsQuantumCaptureTests() =>
|
||||
PlayerPhysicsQuantumCapture.ResetForTest();
|
||||
|
||||
[Fact]
|
||||
public void DisabledCapture_DoesNotCreateAFile()
|
||||
{
|
||||
PhysicsBody body = CreateBody();
|
||||
|
||||
Assert.False(PlayerPhysicsQuantumCapture.IsEnabled);
|
||||
_ = PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
Assert.False(File.Exists(_path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnabledCapture_WritesCompleteOrderedQuantum()
|
||||
{
|
||||
PhysicsBody body = CreateBody();
|
||||
PlayerPhysicsQuantumCapture.CapturePath = _path;
|
||||
|
||||
PlayerPhysicsBodyTraceSnapshot start =
|
||||
PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
body.Velocity = new Vector3(2f, 3f, -4f);
|
||||
PlayerPhysicsBodyTraceSnapshot pre =
|
||||
PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
body.UpdatePhysicsInternal(0.05f);
|
||||
PlayerPhysicsBodyTraceSnapshot post =
|
||||
PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
body.Velocity = new Vector3(1f, 1.5f, 0.2f);
|
||||
PlayerPhysicsBodyTraceSnapshot committed =
|
||||
PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
|
||||
PlayerPhysicsQuantumCapture.Log(
|
||||
dt: 0.05f,
|
||||
cellBefore: 0xAAB40011,
|
||||
cellAfter: 0xAAB40012,
|
||||
input: new MovementInput(Forward: true, Run: true),
|
||||
rootAndManagerDelta: new Vector3(0.1f, 0.2f, 0.3f),
|
||||
candidateMoved: true,
|
||||
quantumStart: start,
|
||||
preIntegration: pre,
|
||||
postIntegration: post,
|
||||
resolve: new PlayerPhysicsResolveTraceSnapshot(
|
||||
new Vector3(4f, 5f, 6f),
|
||||
0xAAB40012,
|
||||
Ok: true,
|
||||
IsOnGround: true,
|
||||
InContact: true,
|
||||
OnWalkable: true,
|
||||
CollisionNormalValid: true,
|
||||
CollisionNormal: Vector3.UnitZ),
|
||||
postCommit: committed);
|
||||
PlayerPhysicsQuantumCapture.Close();
|
||||
|
||||
string[] lines = File.ReadAllLines(_path);
|
||||
Assert.Single(lines);
|
||||
|
||||
using JsonDocument json = JsonDocument.Parse(lines[0]);
|
||||
JsonElement root = json.RootElement;
|
||||
Assert.Equal(0, root.GetProperty("sequence").GetInt64());
|
||||
Assert.Equal(0.05f, root.GetProperty("dt").GetSingle());
|
||||
Assert.Equal(0xAAB40011u, root.GetProperty("cellBefore").GetUInt32());
|
||||
Assert.Equal(0xAAB40012u, root.GetProperty("cellAfter").GetUInt32());
|
||||
Assert.True(root.GetProperty("input").GetProperty("forward").GetBoolean());
|
||||
Assert.True(root.GetProperty("input").GetProperty("run").GetBoolean());
|
||||
Assert.Equal(2f,
|
||||
root.GetProperty("preIntegration")
|
||||
.GetProperty("velocity")
|
||||
.GetProperty("x")
|
||||
.GetSingle());
|
||||
Assert.True(root.GetProperty("resolve").GetProperty("onWalkable").GetBoolean());
|
||||
Assert.Equal(0.2f,
|
||||
root.GetProperty("postCommit")
|
||||
.GetProperty("velocity")
|
||||
.GetProperty("z")
|
||||
.GetSingle());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetForTest_ClosesWriterAndRestartsSequence()
|
||||
{
|
||||
PhysicsBody body = CreateBody();
|
||||
PlayerPhysicsQuantumCapture.CapturePath = _path;
|
||||
PlayerPhysicsBodyTraceSnapshot snapshot =
|
||||
PlayerPhysicsQuantumCapture.Snapshot(body);
|
||||
|
||||
WriteMinimal(snapshot);
|
||||
PlayerPhysicsQuantumCapture.ResetForTest();
|
||||
PlayerPhysicsQuantumCapture.CapturePath = _path;
|
||||
WriteMinimal(snapshot);
|
||||
PlayerPhysicsQuantumCapture.Close();
|
||||
|
||||
string[] lines = File.ReadAllLines(_path);
|
||||
Assert.Equal(2, lines.Length);
|
||||
Assert.All(lines, line =>
|
||||
{
|
||||
using JsonDocument json = JsonDocument.Parse(line);
|
||||
Assert.Equal(0, json.RootElement.GetProperty("sequence").GetInt64());
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PlayerPhysicsQuantumCapture.ResetForTest();
|
||||
if (File.Exists(_path))
|
||||
File.Delete(_path);
|
||||
}
|
||||
|
||||
private void WriteMinimal(PlayerPhysicsBodyTraceSnapshot snapshot) =>
|
||||
PlayerPhysicsQuantumCapture.Log(
|
||||
0.04f,
|
||||
1,
|
||||
1,
|
||||
default,
|
||||
Vector3.Zero,
|
||||
false,
|
||||
snapshot,
|
||||
snapshot,
|
||||
snapshot,
|
||||
new PlayerPhysicsResolveTraceSnapshot(
|
||||
snapshot.Position,
|
||||
1,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Vector3.Zero),
|
||||
snapshot);
|
||||
|
||||
private static PhysicsBody CreateBody() => new()
|
||||
{
|
||||
Position = new Vector3(1f, 2f, 3f),
|
||||
Velocity = new Vector3(4f, 5f, 6f),
|
||||
Acceleration = new Vector3(0f, 0f, -9.8f),
|
||||
GroundNormal = Vector3.UnitZ,
|
||||
ContactPlaneValid = true,
|
||||
ContactPlane = new Plane(Vector3.UnitZ, -3f),
|
||||
Friction = 0.95f,
|
||||
Elasticity = 0.05f,
|
||||
State = PhysicsStateFlags.Gravity,
|
||||
TransientState =
|
||||
TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
};
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class PlayerPhysicsQuantumCaptureCollection
|
||||
{
|
||||
public const string Name = "Player physics quantum capture";
|
||||
}
|
||||
125
tools/analyze_269_slope_stop_capture.py
Normal file
125
tools/analyze_269_slope_stop_capture.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Summarize an ACDREAM_CAPTURE_PLAYER_QUANTA JSONL for issue #269."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def vector(value: dict[str, Any]) -> tuple[float, float, float]:
|
||||
return float(value["x"]), float(value["y"]), float(value["z"])
|
||||
|
||||
|
||||
def length(value: tuple[float, float, float]) -> float:
|
||||
return math.sqrt(sum(component * component for component in value))
|
||||
|
||||
|
||||
def horizontal(value: tuple[float, float, float]) -> float:
|
||||
return math.hypot(value[0], value[1])
|
||||
|
||||
|
||||
def subtract(
|
||||
left: tuple[float, float, float],
|
||||
right: tuple[float, float, float],
|
||||
) -> tuple[float, float, float]:
|
||||
return tuple(a - b for a, b in zip(left, right, strict=True))
|
||||
|
||||
|
||||
def scale(
|
||||
value: tuple[float, float, float],
|
||||
scalar: float,
|
||||
) -> tuple[float, float, float]:
|
||||
return tuple(component * scalar for component in value)
|
||||
|
||||
|
||||
def input_active(record: dict[str, Any]) -> bool:
|
||||
state = record["input"]
|
||||
return any(
|
||||
bool(state[key])
|
||||
for key in (
|
||||
"forward",
|
||||
"backward",
|
||||
"strafeLeft",
|
||||
"strafeRight",
|
||||
"turnLeft",
|
||||
"turnRight",
|
||||
"jump",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("capture", type=Path)
|
||||
parser.add_argument(
|
||||
"--tail",
|
||||
type=int,
|
||||
default=45,
|
||||
help="quanta printed after the final directional-input release",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
with args.capture.open("r", encoding="utf-8") as stream:
|
||||
records = [json.loads(line) for line in stream if line.strip()]
|
||||
|
||||
if not records:
|
||||
print("capture contains no physics quanta")
|
||||
return 2
|
||||
|
||||
release_indices = [
|
||||
index
|
||||
for index in range(1, len(records))
|
||||
if input_active(records[index - 1]) and not input_active(records[index])
|
||||
]
|
||||
start = release_indices[-1] if release_indices else max(0, len(records) - args.tail)
|
||||
stop = min(len(records), start + args.tail)
|
||||
|
||||
print(
|
||||
"seq dt walk(pre/post) |v|pre |v|fric |v|commit "
|
||||
"horizCommit rootDelta collision fsf"
|
||||
)
|
||||
for record in records[start:stop]:
|
||||
dt = float(record["dt"])
|
||||
pre = record["preIntegration"]
|
||||
post_integration = record["postIntegration"]
|
||||
post_commit = record["postCommit"]
|
||||
pre_velocity = vector(pre["velocity"])
|
||||
integrated_velocity = vector(post_integration["velocity"])
|
||||
acceleration = vector(pre["acceleration"])
|
||||
friction_velocity = subtract(
|
||||
integrated_velocity,
|
||||
scale(acceleration, dt),
|
||||
)
|
||||
commit_velocity = vector(post_commit["velocity"])
|
||||
transient_pre = int(pre["transientState"])
|
||||
transient_post = int(post_commit["transientState"])
|
||||
pre_walk = (transient_pre & 0x2) != 0
|
||||
post_walk = (transient_post & 0x2) != 0
|
||||
root_delta = length(vector(record["rootAndManagerDelta"]))
|
||||
resolve = record["resolve"]
|
||||
collision = "yes" if resolve["collisionNormalValid"] else "no"
|
||||
print(
|
||||
f'{record["sequence"]:5d} {dt:0.5f} '
|
||||
f"{int(pre_walk)}/{int(post_walk)} "
|
||||
f"{length(pre_velocity):8.4f} "
|
||||
f"{length(friction_velocity):8.4f} "
|
||||
f"{length(commit_velocity):8.4f} "
|
||||
f"{horizontal(commit_velocity):8.4f} "
|
||||
f"{root_delta:8.4f} {collision:>3s} "
|
||||
f'{post_commit["framesStationaryFall"]:d}'
|
||||
)
|
||||
|
||||
if release_indices:
|
||||
print(f"\nlast directional-input release: sequence {records[start]['sequence']}")
|
||||
else:
|
||||
print("\nno directional-input release edge found; showing capture tail")
|
||||
print(f"records: {len(records)}, displayed: {stop - start}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
48
tools/cdb/issue269-slope-stop.cdb
Normal file
48
tools/cdb/issue269-slope-stop.cdb
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
$$
|
||||
$$ Issue #269 retail slope-stop trace.
|
||||
$$
|
||||
$$ This script must only be attached to the Sept 2013 EoR acclient.exe
|
||||
$$ paired with refs/acclient.pdb (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32).
|
||||
$$ The PowerShell runner verifies that pairing before attach.
|
||||
$$
|
||||
$$ It records the player object's complete physics-integrator boundary and
|
||||
$$ handle_all_collisions boundary as raw IEEE-754 bits. That is the minimum
|
||||
$$ runtime evidence needed to distinguish friction cadence, contact loss, and
|
||||
$$ post-sweep collision response without guessing from visible distance.
|
||||
$$
|
||||
|
||||
.logopen ${ARG_LOG_PATH}
|
||||
.sympath ${ARG_SYMBOL_PATH}
|
||||
.symopt+ 0x40
|
||||
.reload /f acclient.exe
|
||||
|
||||
r $t0 = 0
|
||||
r $t1 = 0
|
||||
|
||||
$$ CPhysicsObj offsets from PDB dt:
|
||||
$$ state +0xa8, transient +0xac, friction +0xbc,
|
||||
$$ velocity +0xe0, acceleration +0xec, contact_plane.N +0x130.
|
||||
$$ UpdatePhysicsInternal entry: ecx=this, [esp+4]=dt, [esp+8]=Frame*.
|
||||
bp acclient!CPhysicsObj::UpdatePhysicsInternal ".if (@ecx == poi(acclient!CPhysicsObj::player_object)) { r $t0=@$t0+1; .printf \"[UPI-IN] q=%d dt_h=%08X state=%08X transient=%08X friction_h=%08X vx_h=%08X vy_h=%08X vz_h=%08X ax_h=%08X ay_h=%08X az_h=%08X nx_h=%08X ny_h=%08X nz_h=%08X fx_h=%08X fy_h=%08X fz_h=%08X\\n\", @$t0, dwo(@esp+4), dwo(@ecx+0xa8), dwo(@ecx+0xac), dwo(@ecx+0xbc), dwo(@ecx+0xe0), dwo(@ecx+0xe4), dwo(@ecx+0xe8), dwo(@ecx+0xec), dwo(@ecx+0xf0), dwo(@ecx+0xf4), dwo(@ecx+0x130), dwo(@ecx+0x134), dwo(@ecx+0x138), dwo(poi(@esp+8)+0x34), dwo(poi(@esp+8)+0x38), dwo(poi(@esp+8)+0x3c) }; gc"
|
||||
|
||||
$$ UpdatePhysicsInternal epilogue: edi=this, ebx=Frame*, velocity has
|
||||
$$ completed friction + acceleration and Frame contains the integrated delta.
|
||||
$$ The terminal hit intentionally omits gc so the top-level qd detaches cleanly.
|
||||
bp acclient+0x0011093a ".if (@edi == poi(acclient!CPhysicsObj::player_object)) { .printf \"[UPI-OUT] q=%d transient=%08X vx_h=%08X vy_h=%08X vz_h=%08X fx_h=%08X fy_h=%08X fz_h=%08X\\n\", @$t0, dwo(@edi+0xac), dwo(@edi+0xe0), dwo(@edi+0xe4), dwo(@edi+0xe8), dwo(@ebx+0x34), dwo(@ebx+0x38), dwo(@ebx+0x3c); .if (@$t0 < ${ARG_MAX_QUANTA}) { gc } } .else { gc }"
|
||||
|
||||
$$ handle_all_collisions entry: ecx=this, [esp+4]=COLLISIONINFO*.
|
||||
$$ COLLISIONINFO offsets from PDB dt: normal-valid +0x48,
|
||||
$$ normal +0x4c, frames_stationary_fall +0x80.
|
||||
bp acclient!CPhysicsObj::handle_all_collisions ".if (@ecx == poi(acclient!CPhysicsObj::player_object)) { r $t1=@$t1+1; .printf \"[HAC-IN] h=%d q=%d fsf=%d normalValid=%d nx_h=%08X ny_h=%08X nz_h=%08X vx_h=%08X vy_h=%08X vz_h=%08X transient=%08X\\n\", @$t1, @$t0, dwo(poi(@esp+4)+0x80), dwo(poi(@esp+4)+0x48), dwo(poi(@esp+4)+0x4c), dwo(poi(@esp+4)+0x50), dwo(poi(@esp+4)+0x54), dwo(@ecx+0xe0), dwo(@ecx+0xe4), dwo(@ecx+0xe8), dwo(@ecx+0xac) }; gc"
|
||||
|
||||
$$ Four epilogues correspond to fsf 0, 1, 2, and 3. At each address esi
|
||||
$$ still owns this and the final velocity/transient state has been written.
|
||||
bp acclient+0x00114977 ".if (@esi == poi(acclient!CPhysicsObj::player_object)) { .printf \"[HAC-OUT] h=%d q=%d fsfClass=0 vx_h=%08X vy_h=%08X vz_h=%08X transient=%08X\\n\", @$t1, @$t0, dwo(@esi+0xe0), dwo(@esi+0xe4), dwo(@esi+0xe8), dwo(@esi+0xac) }; gc"
|
||||
bp acclient+0x00114997 ".if (@esi == poi(acclient!CPhysicsObj::player_object)) { .printf \"[HAC-OUT] h=%d q=%d fsfClass=1 vx_h=%08X vy_h=%08X vz_h=%08X transient=%08X\\n\", @$t1, @$t0, dwo(@esi+0xe0), dwo(@esi+0xe4), dwo(@esi+0xe8), dwo(@esi+0xac) }; gc"
|
||||
bp acclient+0x001149b1 ".if (@esi == poi(acclient!CPhysicsObj::player_object)) { .printf \"[HAC-OUT] h=%d q=%d fsfClass=2 vx_h=%08X vy_h=%08X vz_h=%08X transient=%08X\\n\", @$t1, @$t0, dwo(@esi+0xe0), dwo(@esi+0xe4), dwo(@esi+0xe8), dwo(@esi+0xac) }; gc"
|
||||
bp acclient+0x001149c6 ".if (@esi == poi(acclient!CPhysicsObj::player_object)) { .printf \"[HAC-OUT] h=%d q=%d fsfClass=3 vx_h=%08X vy_h=%08X vz_h=%08X transient=%08X\\n\", @$t1, @$t0, dwo(@esi+0xe0), dwo(@esi+0xe4), dwo(@esi+0xe8), dwo(@esi+0xac) }; gc"
|
||||
|
||||
.printf "issue269 slope-stop probe armed; maxQuanta=${ARG_MAX_QUANTA}\\n"
|
||||
g
|
||||
.echo === DETACHING AFTER BOUNDED CAPTURE ===
|
||||
qd
|
||||
74
tools/cdb/run-issue269-slope-stop.ps1
Normal file
74
tools/cdb/run-issue269-slope-stop.ps1
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidatePattern('^[A-Za-z0-9_.-]+$')]
|
||||
[string]$ScenarioTag = "slope-stop",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateRange(60, 1800)]
|
||||
[int]$MaxQuanta = 450
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$cdbExe = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"
|
||||
if (-not (Test-Path -LiteralPath $cdbExe)) {
|
||||
throw "cdb.exe was not found at '$cdbExe'."
|
||||
}
|
||||
|
||||
$retail = Get-CimInstance Win32_Process -Filter "Name = 'acclient.exe'" |
|
||||
Select-Object -First 1
|
||||
if ($null -eq $retail) {
|
||||
throw "No live retail acclient.exe process was found."
|
||||
}
|
||||
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$checkScript = Join-Path $repoRoot "tools\pdb-extract\check_exe_pdb.py"
|
||||
$pairing = & py $checkScript $retail.ExecutablePath 2>&1 | Out-String
|
||||
if ($pairing -notmatch "=== MATCH:") {
|
||||
throw @"
|
||||
The live retail executable does not pair with the named-retail PDB.
|
||||
Process path: $($retail.ExecutablePath)
|
||||
$pairing
|
||||
"@
|
||||
}
|
||||
|
||||
$symbolCandidates = @(
|
||||
(Join-Path $repoRoot "refs"),
|
||||
(Join-Path $env:USERPROFILE "source\repos\acdream\refs"),
|
||||
(Join-Path $env:USERPROFILE ".windbg\x86\sym")
|
||||
)
|
||||
$symbolPath = $symbolCandidates |
|
||||
Where-Object { Test-Path -LiteralPath $_ } |
|
||||
Select-Object -First 1
|
||||
if ([string]::IsNullOrWhiteSpace($symbolPath)) {
|
||||
throw "The matching acclient.pdb symbol directory could not be found."
|
||||
}
|
||||
|
||||
$templatePath = Join-Path $PSScriptRoot "issue269-slope-stop.cdb"
|
||||
$artifactDir = Join-Path $repoRoot "artifacts\issue269"
|
||||
New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$logPath = Join-Path $artifactDir "$ScenarioTag-retail-$timestamp.log"
|
||||
$scriptPath = Join-Path $env:TEMP "issue269-$ScenarioTag-$timestamp.cdb"
|
||||
|
||||
$script = Get-Content -LiteralPath $templatePath -Raw
|
||||
$script = $script.Replace('${ARG_LOG_PATH}', $logPath)
|
||||
$script = $script.Replace('${ARG_SYMBOL_PATH}', $symbolPath)
|
||||
$script = $script.Replace('${ARG_MAX_QUANTA}', $MaxQuanta.ToString(
|
||||
[System.Globalization.CultureInfo]::InvariantCulture))
|
||||
Set-Content -LiteralPath $scriptPath -Value $script -Encoding ASCII
|
||||
|
||||
Write-Host "Attaching cdb to retail PID $($retail.ProcessId)."
|
||||
Write-Host "Capture: $logPath"
|
||||
Write-Host "Perform the slope run, release movement, and let the character settle."
|
||||
Write-Host "The probe detaches after $MaxQuanta player physics quanta."
|
||||
|
||||
try {
|
||||
& $cdbExe -p $retail.ProcessId -cf $scriptPath 2>&1 |
|
||||
Out-File -LiteralPath "$logPath.console" -Encoding ASCII
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $scriptPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Retail capture complete: $logPath"
|
||||
Loading…
Add table
Add a link
Reference in a new issue