docs(physics): #347 closed WITHOUT a code change — retail's glide alternates exactly as ours does; AD-70 retired as a wrong inference
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The round-2 cdb capture is decisive: during a live retail glide, edge_slide fired ~1.5 times per find_transitional_position — the arm/move alternation's exact signature (3 entries on the arming tick, 0 on the moving tick) — with cliff_slide in lockstep, step_down at 2.5x, step_up 0, and every stack sample on our identical call path. cliff_slide's bytes match our port and ACE's (compare constant at 0x794610 verified 0.0), and the user could not distinguish the two clients side by side. The "retail redirects within the tick" premise misread round-1's set_sliding_normal cadence (per-event, not per-tick, so its 1:1 ratio with edge never discriminated anything). The alternation-tolerant assertion in Issue345SteepSlopeGlideTests is therefore the CORRECT retail-shape pin from both sides; its comment now cites the capture instead of calling the shape a residual. The #269 note is honest the other way: the hope that a within-tick port would explain that feel residual is withdrawn with the premise. The temporary Scratch347 diagnostic is deleted. Capture evidence: 345-glide-stacks.cdb.log (repo root, untracked, cited from the contract's RESOLUTION section). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
535f41bbdf
commit
fa0c053ebf
5 changed files with 44 additions and 78 deletions
|
|
@ -158,9 +158,30 @@ resolution.
|
|||
|
||||
---
|
||||
|
||||
## #347 — Steep-slope glide runs at HALF retail's rate: arm/move alternation instead of within-tick redirect
|
||||
## #347 — Steep-slope glide alternation — CLOSED 2026-08-08: RETAIL DOES THE SAME; the "half-rate" premise was a wrong inference
|
||||
|
||||
**Status:** OPEN — MEDIUM (behavioral residual of the #345 fix, trace-backed).
|
||||
**The round-2 cdb capture (`345-glide-stacks.cdb.log`) closes this without
|
||||
a code change.** During the glide window retail fired ~145 edge_slide
|
||||
entries per ~100 find_transitional_position calls — ~1.5 per player tick,
|
||||
which is EXACTLY the arm/move alternation's signature (3 entries on the
|
||||
arming tick, 0 on the moving tick). cliff_slide ran in strict lockstep,
|
||||
step_down at ~2.5x edge (our 2-probe plan + edge's internal call), step_up
|
||||
0, and the six stack samples show the identical call path
|
||||
(transitional_insert -> find_transitional_position -> CPhysicsObj::
|
||||
transition). Combined with cliff_slide's arms being byte-identical across
|
||||
ACE/acdream/the raw binary (compare constant at 0x794610 verified 0.0) and
|
||||
the user's side-by-side observation ("I cant detect any speed change from
|
||||
retail"), the conclusion is that retail alternates exactly as we do —
|
||||
dig-retries included. The original "retail redirects within the tick"
|
||||
premise came from misreading the round-1 counters (set_sliding_normal's
|
||||
cadence is per-event, not per-tick, so its 1:1 ratio with edge never
|
||||
discriminated anything). AD-70 is retired as a wrong inference, not fixed.
|
||||
The alternation-tolerant assertion in `Issue345SteepSlopeGlideTests` is
|
||||
the CORRECT retail-shape pin and stays. Consequence for #269: the hope
|
||||
that a within-tick port would explain the slope-slide feel residual is
|
||||
withdrawn — #269 keeps its original "needs a live cdb A/B trace" plan.
|
||||
|
||||
**Original filing (premise since refuted):**
|
||||
With #345 fixed, the glide works but alternates in a strict two-tick cycle:
|
||||
the arming tick absorbs the whole request while the edge response sets the
|
||||
sliding normal ((0.707,-0.707,0) on the conformance fixture), and only the
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ readiness/requeue adaptation. See
|
|||
| AD-67 | **Filed 2026-08-07 at the #32 closeout.** The narrowed `CollisionInfo.SetContactPlane` still writes `ContactPlaneCellId`, which retail's `COLLISIONINFO::set_contact_plane` @0x00509d80 does not — retail writes the cell id only in `CTransition::init_contact_plane` (@0x0050e8ca). Kept deliberately at the #32 fix on the research doc's own advice: acdream's consumers (the `[support]` probe's provenance, water-plane bookkeeping, `AdjustOffset`'s `ContactPlaneCellId != 0` gate) rely on the cell id being current per contact write, and retail's equivalent state travels a different route the port has not needed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SetContactPlane`, the `ContactPlaneCellId = cellId` line) | The #32 fix removed the four LAST-KNOWN writes — the defect — and deliberately did not also change this contact-group field in the same commit; two behaviour changes in one fix would have made the user's cliff gate ambiguous. | A consumer that assumes the cell id changes ONLY at transition seed time (retail's timing) would observe it changing per contact write instead. No such consumer is known; `AdjustOffset`'s gate wants the current value. | `COLLISIONINFO::set_contact_plane` 0x00509d80 (22 bytes, no cell-id write); `CTransition::init_contact_plane` 0x0050e850 (cell id at 0x0050e8ca) |
|
||||
| AD-68 | **Filed 2026-08-07 at the #338 closure.** During an entity's ASYNC-RESIDENCY window — its flat Setup collision not yet resident — `LiveEntityMotionRuntimeController.GetSetupMoverShape` returns a placeholder mover shape: empty sphere list (falling back to the legacy 0.48/1.835 capsule reconstruction) and step heights **0.4/0.4**, values that appear nowhere in retail (authored human values are 0.600/1.500; retail's not-on-walkable fallback is 0.04). The local player has the same window between controller construction (0.4f defaults) and the publication candidate's adoption. Retail loads Setups synchronously and has no such window at all. Measured scale: 358 placeholder resolves vs 111,248 authored-pair resolves across one long session — seconds per entity, once. | `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`GetSetupMoverShape`, the `setup is null` and `<= 0f` arms); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (0.4f field defaults, adopted-over at publication) | An adaptation to async residency, not a wiring defect — #338's live probe proved prepare/publish/resolve all carry the authored values in steady state. Left as-is deliberately: shrinking the window is streaming work, not physics work. | A remote moving DURING its residency window steps 0.4 instead of its authored heights, and collides as a capsule instead of its sphere list — briefly, once per entity. If a future report says "an NPC stumbled on a stair right as it appeared", this row is the first suspect. | `CTransition::step_up` 0x0050b610 (0.04 fallback at 0x0050b655); `CPartArray::GetStepUpHeight` 0x005180d0; issue #338 |
|
||||
| AD-69 | **Filed 2026-08-07 at the S4 pseudocode pass (implementer finding, verified against the decomp).** `Transition.AdjustOffset`'s safety push-out computes `dist` WITHOUT the cell-relative correction retail applies: retail's `adjust_offset` (and ACE's port, independently) run the sphere centre through `LandDefs::get_block_offset` against the contact plane's own cell before the plane-distance dot, so a contact plane owned by a DIFFERENT landblock than the mover's current cell measures in the plane's frame. acdream dots the raw world-space centre against the stored plane. Same-landblock contact (the overwhelming case) is identical; a landblock-SEAM contact measures dist offset by the block delta, mis-firing or mis-suppressing the push-out at seams. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`AdjustOffset`, the dist computation ahead of the push-out block) | Discovered during S4 but deliberately not folded in: S4's own AD-66 half was withheld the same night (#341), and a third change in the same block would have made the anomaly investigation unattributable. Fix alongside the AD-66 relanding. | A mover resting on a contact plane owned by the neighbouring landblock (seam walking) gets a push-out computed against a dist that is wrong by the block offset — either a spurious lift or a missed penetration correction, exactly at landblock seams, the #176/#177 symptom neighbourhood. | `CTransition::adjust_offset` 0x0050a370 (pc:272271-272393); `LandDefs::get_block_offset`; ACE `Transition.AdjustOffset` (cross-check); issue #341 (sequencing) |
|
||||
| AD-70 | **Filed 2026-08-08 with the #345 fix.** Our steep-slope glide alternates: the edge-family arming tick absorbs the request (zero yield) and only the next tick's `AdjustOffset` pre-projection moves, then the clean move clears the sliding normal — a strict two-tick cycle. Retail redirects WITHIN the tick (`edge_slide`/`cliff_slide` 594 each over a ~15 s live glide — every 30 Hz tick, lockstep with `set_sliding_normal` 538) and yields motion every tick. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed` + the insert's post-constraint continuation) | The #345 landing deliberately touched only `validate_walkable`'s return scoping; the response bodies were freshly user-gated (Campaign S) and AD-66 had just relanded in the same block. | Gliding along a too-steep face at ~half retail's lateral speed; direction and angle-scaling correct. Visible as "slides but slower than retail" in a side-by-side. | `345-retail-glide.cdb.log` counters; `Issue345SteepSlopeGlideTests` tick trace; issue #347 |
|
||||
| ~~AD-70~~ | **RETIRED 2026-08-08 (same day, round-2 cdb capture): the row described retail behavior, not a divergence.** Retail's glide alternates exactly as ours does — the capture measured ~1.5 edge_slide entries per find_transitional_position during the glide (the alternation's exact signature: 3 on the arming tick, 0 on the moving tick), lockstep cliff_slide, step_down at 2.5x, and identical stack paths; cliff_slide's bytes match our port and ACE's. The 'retail redirects within the tick' inference misread round-1's set_sliding_normal cadence (per-event, not per-tick). Issue #347 closed without a code change. **Filed 2026-08-08 with the #345 fix.** Our steep-slope glide alternates: the edge-family arming tick absorbs the request (zero yield) and only the next tick's `AdjustOffset` pre-projection moves, then the clean move clears the sliding normal — a strict two-tick cycle. Retail redirects WITHIN the tick (`edge_slide`/`cliff_slide` 594 each over a ~15 s live glide — every 30 Hz tick, lockstep with `set_sliding_normal` 538) and yields motion every tick. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed` + the insert's post-constraint continuation) | The #345 landing deliberately touched only `validate_walkable`'s return scoping; the response bodies were freshly user-gated (Campaign S) and AD-66 had just relanded in the same block. | Gliding along a too-steep face at ~half retail's lateral speed; direction and angle-scaling correct. Visible as "slides but slower than retail" in a side-by-side. | `345-retail-glide.cdb.log` counters; `Issue345SteepSlopeGlideTests` tick trace; issue #347 |
|
||||
| AD-71 | **Filed 2026-08-08 (reviewer finding on the #345 fix).** `ValidateWalkable`'s walkable test uses the MUTABLE `sp.WalkableAllowance` where retail's `validate_walkable` calls `CPhysicsObj::is_valid_walkable` @0x0050f530 — a FIXED global threshold (N.z >= [0x8ede5c], the walkable constant; the function reads no object state). Several code paths write `WalkableAllowance = LandingZ` (0.0871557 — TransitionTypes.cs:1688,2264, BSPQuery.cs:2330, FlatBspQuery.cs:2085) and `ClearWalkable()` does not restore it, so a stale-permissive value entering a grounded `!StepDown && OnWalkable` validate makes the guard PASS where retail's fails. Every override is permissive, so the #345 fix cannot REGRESS through this path — but for planes with N.z in (0.0872, 0.6642) a stale allowance leaves the old Adjusted-without-push dead loop reachable. The #345 landing GREW this row's blast radius: the operand now gates the return value (OK vs Adjusted), not merely the push (reviewer B, 2026-08-08). Also folds in: our `FloorZ = 0.6642f` vs ACE's 0.66417414f flips OK/Adjusted in a ~0.002-degree band. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ValidateWalkable`, the `walkable` guard operand) | Deliberately not folded into the #345 landing: the allowance plumbing is shared with the step-down family and needs its own conformance pass over every WalkableAllowance write/restore site. | A too-steep plane between LandingZ and FloorZ validated right after a placement/landing path that left the allowance permissive: the guard pushes+Adjusts where retail returns OK — the #345 stop, in a narrower band. | capstone decode of 0x0050f530 (reviewer A, 2026-08-08); `docs/research/2026-08-08-345-d0-branch-pin.md` flagged-secondary section |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -165,3 +165,18 @@ round-2 script (`tools/cdb/345-glide-stacks.cdb`) now counts ftp, prints
|
|||
the periodic progression, samples 6 stacks each for edge/cliff/step_down,
|
||||
and auto-detaches at 300 edge hits via the fall-through-then-top-level-qd
|
||||
recipe. NO code changes until this capture runs.
|
||||
|
||||
## RESOLUTION (2026-08-08, round-2 capture): H-A confirmed in its strong form — CLOSED, no code change
|
||||
|
||||
Round-2 (`345-glide-stacks.cdb.log`, auto-detached at 768 edge hits):
|
||||
glide-window steady state per 1280-vwalk block: edge +133..158, ftp
|
||||
+93..105, stepdown +351..376, cliff lockstep with edge, stepup 0. edge/ftp
|
||||
~ 1.45 with ftp INCLUDING background movers — the player's true ratio is
|
||||
~1.5, which is precisely the alternation's signature (3 arming-tick
|
||||
entries, 0 moving-tick entries, averaged). All six edge_slide stack
|
||||
samples: transitional_insert -> find_transitional_position ->
|
||||
CPhysicsObj::transition -> UpdateObjectInternal — our exact path. Retail
|
||||
performs the same dig-retries and the same alternate-tick yield. #347
|
||||
closed; AD-70 retired; the D2 "tighten to full-rate" plan is CANCELLED —
|
||||
the existing alternation-tolerant assertion is the correct retail pin.
|
||||
The H-B mechanism does not exist in the binary's behavior.
|
||||
|
|
|
|||
|
|
@ -82,9 +82,11 @@ public sealed class Issue345SteepSlopeGlideTests
|
|||
// 28 of 30 here). The fixed glide alternates: the arming tick
|
||||
// absorbs the request while edge-slide sets the sliding normal,
|
||||
// the next tick consumes it and moves (14 of 30 stuck). The
|
||||
// alternation itself is a KNOWN half-rate residual vs retail's
|
||||
// within-tick redirect (#347) — this assertion only rejects the
|
||||
// dead loop. Stuck ticks are counted from positions, not the
|
||||
// alternation IS retail's own shape — confirmed by the #347
|
||||
// round-2 cdb capture (345-glide-stacks.cdb.log: ~1.5 edge_slide
|
||||
// per find_transitional_position during a live glide, the
|
||||
// alternation's exact signature) — so this range pins it from
|
||||
// both sides. Stuck ticks are counted from positions, not the
|
||||
// (temporary) transit-fail probe, so the assertion survives the
|
||||
// probe family's eventual strip; the lower bound keeps it from
|
||||
// going vacuous if the fixture stops engaging the face at all.
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// TEMPORARY #347 diagnostic — deleted before landing.
|
||||
public sealed class Scratch347DiagTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
public Scratch347DiagTests(ITestOutputHelper output) => _out = output;
|
||||
|
||||
[Fact]
|
||||
public void DumpFirstArmingTick()
|
||||
{
|
||||
var heights = new byte[81];
|
||||
heights[3 * 9 + 4] = 32;
|
||||
var heightTable = new float[256];
|
||||
for (int i = 0; i < 256; i++) heightTable[i] = i;
|
||||
|
||||
var engine = new PhysicsEngine();
|
||||
engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(heights, heightTable),
|
||||
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
|
||||
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.Gravity,
|
||||
TransientState = TransientStateFlags.Active | TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
};
|
||||
|
||||
float dx = 0f, dy = 0.23f;
|
||||
var position = new Vector3(80.4f, 79.8f, 0f);
|
||||
uint cell = TerrainSurface.ComputeOutdoorCellId(0xA9B4FFFFu, 80.4f, 79.8f);
|
||||
|
||||
PhysicsDiagnostics.DumpTransitFailEnabled = true;
|
||||
var saved = Console.Out;
|
||||
var sw = new StringWriter();
|
||||
Console.SetOut(sw);
|
||||
try
|
||||
{
|
||||
for (int tick = 0; tick < 3; tick++)
|
||||
{
|
||||
var result = engine.ResolveWithTransition(
|
||||
currentPos: position,
|
||||
targetPos: new Vector3(position.X + dx, position.Y + dy, position.Z),
|
||||
cellId: cell,
|
||||
sphereRadius: 0.47f,
|
||||
sphereHeight: 1.20f,
|
||||
stepUpHeight: 0.60f,
|
||||
stepDownHeight: 1.50f,
|
||||
isOnGround: true,
|
||||
body: body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: 0x5000000Au);
|
||||
position = result.Position;
|
||||
cell = result.CellId;
|
||||
if (tick < 2) sw.GetStringBuilder().Clear(); // keep only the arming tick
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(saved);
|
||||
PhysicsDiagnostics.DumpTransitFailEnabled = false;
|
||||
}
|
||||
|
||||
foreach (var line in sw.ToString().Split('\n'))
|
||||
_out.WriteLine(line.TrimEnd());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue