1466 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e602f84be2 |
fix(ui): Slice 5.4 review corrections — the dropdown renders from its authored popup, retail cost semantics, auto-select, icon overlays
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
All nine review findings closed at root (one sub-item consciously
deferred):
F1 the category dropdown now draws: sprites/fonts wired and the popup
geometry read from the vendor menu's own authored popup LayoutDesc
0x21000043 (root 0x1000034F — correcting the review's 0x1000014F
transcription) per UIElement_Menu::MakePopup (pc:120705); chat's menu
is untouched and its tests prove it. The new test drives selection
through the REAL open/hit path the review flagged as bypassed.
F2+F3 the selected-item cost display ports VendorItemsUI::UpdateItemsUI
verbatim: quantity via the 0xDC41CB0 split-size mask (whole-stack for
ammo, per-unit for groceries/components; mask lives at the toolbar
SEEDING site pc:198784), plural names with retail's
fall-back-to-singular (pc:409056 — correcting the review's "name+s"
guess), full cost sentences with comma grouping and the player's coin
total, and Buy/Add buttons that disable without a selection.
F4 category switches auto-select the first filtered item (pc:201180).
F5 icon underlay/overlay/effects + plural name forwarded from the
already-parsed wire fields through VendorShopItem to the icon
composer. F6 a DIFFERENT vendor opens on its own first category;
same-vendor refresh preserves per the clamp. F7 scroll resets on
rebuild and authored empty slots fill; the right-click examine route
is consciously DEFERRED (shop items are not in ClientObjectTable and
the appraisal panel hard-requires it — documented, not faked).
F8 VendorState.Apply's fanout gets the same per-listener isolation as
Close/Reset. F9 AP-110/AP-161 wording corrected ("quantity-correct
pricing") and AP-161 rewritten to exactly the remaining conscious
gaps.
Clean-room complete solution with the #348 cursor fix in the same
tree: 11,334 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||
|
|
c721830e71 |
feat(ui): Slice 5.4 — the authored vendor browse panel (LayoutDesc 0x21000012)
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 vendor window is retail's own: LayoutDesc 0x21000012, root 0x100000B7, found by enumerating all 101 layouts for the one containing both known tab controls and clinched by the root's Type 0x10000017 — the literal UIElement::RegisterElementClass id for gmVendorUI (pc:202075). Discovery evidence and the D0 read live in the research doc's new §B.4. D0 corrected two assumptions: retail's category "tabs" are a UiMenu DROPDOWN fed by a hardcoded 18-row ordered category table (ported bit-for-bit against our ItemType enum; list always scoped to exactly one category, first-present wins, selection preserved across refresh per retail's clamp), and the layout authors THREE tabs — Items (browse, this slice), Buying and Selling (staged-transaction review, Slice 6) — decision 4's "browse/Buy tab" names the Items tab retail's mode-2 OpenTab opens. The non-default tabs render and switch pages but stay inert, fenced in comments. VendorUiController mounts Items: category dropdown, icon-cell item row with the retained scrollbar, per-unit retail pricing via VendorPricing.SellPrice (the vendor-stock path VendorProfile:: VendorSellPrice feeds), name/cost on selection. The panel is a pure projection of VendorState — opens on populate, closes on clear; the close button's VendorState.Close() is its only permitted mutation. Nothing on the wire. AP-110 narrowed (vendor leaves the absent-panels list); AP-161 files the precise Slice-6 remainder (Buying/Selling unwired, Buy/Add buttons, InqAcceptability). Twelve controller tests on a real-dat fixture. Clean-room complete solution: 11,323 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
609a2dfda0 |
fix(runtime/core): Slice 5.3 review corrections — retirement/transit close, per-unit pricing, guarded auto-close dispatch
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 adversarial review's three blocking findings, each fixed at root: 1. A vendor session now CLOSES when its entity retires (despawn, death, ObjectDelete) and at teleport BEGIN (HasPendingTeleportStart || IsTeleportActive at the existing per-frame seam — both hosts funnel through RuntimeWorldTransitState.TryQueueTeleportStart, which flips the pending flag strictly before activation). The previous permissive early-return stranded the session forever: panel pinned to a stale guid, ActiveVendorId swallowing Use for the rest of the session. 2. VendorShopItem carries the desc's stack size, and VendorPricing.PerUnitValue ports retail's stack-total division (VendorProfile::VendorSellPrice 0x005D1B00: <= 0 guard, integer division) — a stack of 50 arrows now prices per arrow, not at 50x. 3. VendorState.Close() guards its observer fanout with the dispatcher's catch-and-log semantics — a throwing panel listener can no longer propagate into the unprotected per-frame path. Register honesty rides along: the 0.6 m UseRadius fallback was acdream's invention (ACE's CheckClose has no fallback; retail passes the raw authored radius) — removed, the watcher now uses the raw radius and AP-160's citations are corrected and extended with the accepted-position-snapshot cadence; AD-72 files VendorPricing's double-vs-x87-extended narrowing (AD-33's class, bounded by the ±0.1 margin). Nine tests added. Clean-room complete solution: 11,311 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
9796d71522 |
feat(runtime): Slice 5.3 — RuntimeInventoryState owns the vendor browse session
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 sole VendorState joins the J4.2 inventory owners: populated by the new 0x0062 ApproachVendor route (parse via VendorApproach, wire-to- domain mapping at the routing seam, silent-drop on malformed like every sibling), borrowed by both graphical and headless hosts, and torn down through the EXISTING ExternalContainer reset stage — session reset, portal-out, and logout all funnel through the one mechanism. Close is client-local per retail (nothing on the wire): a range watcher rides the existing per-advanced-frame publishMovement callback, using the vendor's own authored UseRadius (ACE's 0.6 m fallback when absent). The dormant ItemInteractionController ActiveVendorId seam is finally wired as a live delegate — real id while open, 0 the moment the session clears. AP-160 filed in this same commit: the watcher measures plain 3D center distance rather than retail's cylinder-gap, because Runtime has no per-NPC collision radius/height source; bounded sub-meter, client- local UI only. Twelve Runtime tests: populate/field mapping, vendor replacement, range clear + within-range retention, all three generation teardowns, the ActiveVendorId seam, malformed-event drop. Clean-room complete solution: 11,302 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
70f37dbd5c |
feat(core): Slice 5.2 — VendorState + retail's exact vendor price math
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
VendorState sits beside ExternalContainerState (contract decision 1) with the same shape: private setters, Changed event, Reset with AggregateException fanout; domain-shaped like ContainerContentEntry since Core cannot reference Core.Net. No Runtime wiring, no UI — 5.3's job. VendorPricing ports ShopSystem::BuyPrice/SellPrice (0x006B6120/ 0x006B6180) faithfully: retail's literal three-way branch survives, including the unreachable-with-real-data negative -1 sentinel that ACE's Math.Max(1, ...) collapse erases — equivalence for legitimate inputs is hand-proven and documented rather than silently assumed. Seven conformance tests with hand-derived golden values (float32 semantics verified independently), covering rate=1.0, fractional rates, value=0, the rounding-sensitive halfway case, stack multipliers, the ItemType rate-override branch, and the sentinel. Clean-room complete solution with 5.1+5.2 in place: 11,291 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
e45c95b06c |
feat(net): Slice 5.1 — ApproachVendor (GameEvent 0x0062) inbound parser
VendorApproach.TryParse reads the vendor profile and the item list per the byte-verified field table (research doc §A.2), each item through the shared PublicWeenieDescParser from 5.0 — zero duplicated parsing. One wire detail the research table did not spell out, found by re-reading ACE's writer and confirmed independently in Chorizite's generated readers: every object body is 4-byte-aligned at its END, so back-to-back vendor items need an explicit AlignTo4 between entries (CreateObject never needed it — nothing follows its body). Pinned by a dedicated test forcing a real 2-byte misalignment via AmmoType. Stack-size sign extension cross-checked against holtburger. Six tests: field-order with distinct literals, empty list, and truncation at each structural boundary — mid-item-tail truncation deliberately inherits 5.0's established non-throwing partial-item contract instead of asserting null everywhere. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
fa0c053ebf |
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
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> |
||
|
|
535f41bbdf |
docs(physics): #347 premise revision — retail may alternate too; ftp:edge ratio is the discriminator
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 cliff_slide arms are conformant in ACE, our port, and the bytes (compare constant at 0x794610 verified 0.0), the round-1 slidn:edge ratio (538:594) refutes a retail retry storm, and the user's side-by-side speed observation fits alternation. Round-2 cdb script now counts find_transitional_position; H-A (identical, retire AD-70) vs H-B (within-tick yield) resolves on one ratio. The temporary Scratch347 diagnostic test rides along until #347 closes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab89ebdf92 |
fix(physics): #345 — a grounded mover glides along a too-steep face; validate_walkable's return is scoped as retail's bytes scope it
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
Retail's OBJECTINFO::validate_walkable @0x0050d010 initializes its return slot to OK (0x0050d025) and assigns ADJUSTED only inside the below-plane guard, immediately after the push executes (0x0050d249). The guard-fail path — grounded, OnWalkable, plane too steep — jumps past the contact write, the push, and the assignment (0x0050d1b9 -> 0x0050d251): retail deliberately IGNORES the steep plane at primary validation so the insert proceeds, the step-down phase fails on the steep landing, and the edge family produces the per-tick lateral glide. ACE flattened this into an unconditional return Adjusted (ObjectInfo.cs:169) and we inherited it; our TransitionalInsert then retried the byte-identical Adjusted forever — the user's stop-instead-of-slide. Evidence chain: the user's retail observation (the axiom), the live cdb glide profile (edge_slide/cliff_slide 594 each in lockstep, step_up 0), the D0 implementer's correct STOP (fixtures reproduced the stuck fingerprint while faithfully executing the ACE-shaped reading — refuting the reading, not the code), and the capstone byte-decode both Opus reviewers re-derived independently, including the stack-slot frame arithmetic and every ret site's eax. The conformance fixture is the live topology: flat and steep terrain triangles sharing ONE cell's diagonal (a cell-boundary face does NOT reproduce the loop — the cell-scoped primary sample never validates a neighbour's triangle — and is pinned as supplementary). Sabotage: restoring the unconditional Adjusted reds the discriminator with the exact stuck position (0.325 m lateral, 28/30 stuck ticks vs 2.602 m / 14/30 fixed; reviewer B's independent five-angle table is monotone 10-85 degrees). Stuck ticks are counted from positions so the assertion survives the eventual probe strip. In-game glide gate PASSED 2026-08-08: "Well it works, we are sliding. I cant detect any speed change from retail." Filed alongside: #347 + AD-70 (our glide alternates arm/move at half retail's per-tick rate — retail redirects within the tick; next up by user direction), AD-71 (the guard's mutable WalkableAllowance operand vs retail's fixed is_valid_walkable global — now return-value-bearing), and the reviewers' named residuals in the #345 closure entry (placement-arm flip, other-cell coverage gap, EdgeSlide-less projectiles, ACE's server-side shared misport predicting remote drift-then-snap on steep terrain). The unported IsViewer arm of validate_walkable is noted in the D0 doc. Suite: clean-room complete solution 11,271 passed / 4 skipped / 0 failed; Core assembly re-run green after the review-driven test hardening. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10efb5b1f9 |
fix(physics): AD-66 relands — the push-out uses retail's bare radius; plant-then-lift complete (#341 closed)
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
Third attempt, landed on evidence where the first two correctly refused: the ten-run stability gate passed 10/10 bit-identical (0x42667451, two clean-room cycles among the runs), the recalibrated golden's every value measured with derivations rather than guessed, and the historical measurement flip stands recorded as unexplained-but-unreproducible after 37 hunt runs plus these 10 found no divergence anywhere. The mechanism, completing the S4b byte-pin: validate_walkable plants the sphere at perpendicular r*N.z (byte-faithful, untouched); this push fires once per settle and lifts to tangent equilibrium dist=r, where the trigger goes quiet — retail's slope hover, arriving via the push exactly as the original substitution's own comment predicted retail had. Sabotage: restoring radius*N.z reddens the discriminating exact-value test verbatim. AD-65 conformance, the uphill no-flap guard, and the #331 absorb pin all green untouched. AD-66 retired (the campaign's last withheld row); AD-69's seam-frame correction deliberately unbundled, stays active as its own follow-up. Clean-room suite 11,267 / 4 / 0 — the suite's two AD-66 skips are gone. User's "port the retail pair" decision is now fully executed; the hover-look slope gate is the remaining acceptance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e761761aa3 |
probe(physics): ACDREAM_DUMP_TRANSIT_FAIL — self-selecting transition-phase trace for #345
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
Fires only on the stuck-tick predicate (>=1mm XY requested, <=0.1mm achieved), buffering per-tick phase outcomes cheaply and flushing only on a stuck tick: per-insert-attempt phase/state/normal/source, step-up enter/exit verdicts, every ValidateWalkable branch with dist/waterDepth and both SetCollisionNormal guards evaluated, and the tick's final AdjustOffset pair. Zero cost when off (flag before any allocation — the I1 zero-alloc gate stays green), mover id on every line, [ThreadStatic] buffer per the referee-safety rule. Two tests: fires on a synthetic wall-stuck tick, silent on ordinary movement. Diagnostics only; no behavioral change. Suite 11,271 / 6 / 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6c6664a685 |
fix(app): #343 — a wounded render loop defers the native release instead of throwing over the real failure
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
Root cause pinned by IL-decompiling Silk.NET.Windowing.Common: ViewImplementationBase._inRenderLoop is set at DoRender/DoUpdate entry and cleared ONLY on normal return, so a throwing frame callback leaves it armed forever and any later Dispose -> Reset throws "You cannot call Reset inside of the render loop", exit 82, replacing the original wounding exception in the report. The fix mirrors Silk's own bracket exactly: GameWindow._renderLoopArmed set at OnUpdate/OnRender entry, cleared only on their normal return — deliberately NOT in a finally, so it tracks the wound the same way Silk's private field does. ReleaseNativeWindow checks it before disposing: armed -> best-effort Close() (swallowed so it can never become the reported failure), no Dispose, and a new terminal status CompleteWithDeferredNativeRelease with Error kept null — the original exception stays the primary report. Healthy paths (OnClosing's in-loop completion, Run()'s tail release) are byte-unchanged, and the new PublishNativeWindow parameters default to null so every existing caller and test behaves identically. Sabotage: disabling the armed-check flipped the deferral test to Expected CompleteWithDeferredNativeRelease / Actual Complete — the guard is what the test exercises. Clean-room suite 11,262 / 6 / 1, the 1 being #340's documented load flake (passed standalone; second recorded firing noted in its entry). Queue: #344 done, #343 done; next #345's instrumented mechanism session, then #341's boundary hunt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
52bdf4df71 |
fix(world): #344 — a mid-teleport world-frame disagreement defers the projection instead of crashing
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
During a portal transit the two world-frame owners legitimately rebase on different edges (Runtime at TeleportAdvanced, streaming only after old-window retirement), and a spawn projection landing in that window hit the #283 invariant as an unhandled render-path throw — the crash the user hit entering a dungeon. The guard's check is unchanged; only the disagreement RESPONSE is discriminated on the canonical transit authority (RuntimeWorldTransitState.IsTeleportActive, the same field the App layer already reads for portal-in-flight): in transit -> the materializer's existing "not yet" return, parking the projection on its established retry rides (OnLandblockLoaded's re-attempt loop, whose ordering guarantees agreement on retry because the recenter coordinator adopts the new origin BEFORE unblocking new landblock loads — verified at source; plus OnPosition recovery and OnAppearance). Outside transit -> still throws: genuine corruption stays loud. The implementer explicitly ruled out riding the Runtime placement pump, which would have acknowledged-and-discarded the completion receipt and silently dropped the entity forever. Sabotage: removing the discriminator reddened the pre-existing #283 throw tests as well as the new not-in-transit test — the sabotage defeats the original contract, not merely the new coverage. Four new tests cover defer, defer-then-agree-then-succeed (projected exactly once), throw-outside-transit, and the agreeing pass-through. Clean-room suite: 11,261 passed / 6 skipped / 0 failed. #346 filed for a sixth, distinct load-sensitive allocation flake observed during the runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c5443b3df9 |
test(physics): S6 — the camera provably reaches both PerfectClip TOI tails; contained, not dormant
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
AP-83/AP-91 claimed no current mover sets PerfectClip. The containment proof found the opposite and the contract's honest-fallback fired: the camera probe (the sole production setter) reaches BOTH ACE-derived tails live — the viewer exemption is creature-only, the shadow-list walk is unconditional, and static scenery with authored primitives is a real non-creature population. Every reach is now recorded (camera-live silently; any non-viewer mover loudly, one-shot), so a future flag change cannot exercise unreviewed ACE-derived math silently. Four tests drive the camera's exact call shape both ways; the sabotage was intelligently adapted — there was no existing cut to disable, so it flips the one axis the proof depends on (IsCreature) and asserts reachability inverts. Both register rows rewritten CONTAINED-not-dormant with severity narrowed to camera-feel (the probe never commits a PhysicsBody). Landing note: diagnostics-only diff (two guard calls + counters + corrected stale comments), verified directly by the session lead rather than a review cycle — the review budget went where behaviour changed tonight. Campaign S CLOSES with this landing: S1A/S1B/S2/S4/S5/S6 done, S3 cancelled, three user-passed gates, one honestly-open item — AD-66's reland, twice self-refused by its own stability gate, blocked on the #341 codegen-shape measurement instability whose ABA evidence and first discriminating experiment are filed. Clean-room suite: 11,257 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9671af0273 |
fix(physics): S2 — static publication emits authored Spheres as Spheres (AP-155 narrowed)
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
Both static sites (LandblockPhysicsPublisher, the headless-only LandblockPhysicsContentBuilder) emitted an authored Setup Sphere as a base-anchored Cylinder of radius r and height 2r. The live path emits a Sphere for the same data, so the same object collided differently by arrival route, and the narrow phase met a flat cap where retail meets a curved surface. Both sites now mirror ShadowShapeBuilder.FromSetup's Sphere block exactly. Combined Opus review: PASS. Its numeric verification of the dispatch test's geometry (head-sphere clearance 0.201 m for the true sphere; the cylinder counterfactual inside by 0.10 m XY with the Z band overlapping) is what makes the discrimination claim more than a sabotage anecdote, and its F8 finding is applied: the test now carries a POSITIVE control — aiming straight through the boulder's centre must block — so a membership/seed regression can no longer masquerade as a curve-hit pass. F4 applied: CylHeight is asserted, not inferred (the C4 lesson). F3 applied: the deleted Quaternion.Inverse base composition is recorded as internally coherent for the old cylinder's world-Z axis — the defect was the shape TYPE, not that rotation math. The review also verified the deleted-cylinder blast radius: the F2 overlay's drawn span is IDENTICAL for both shapes (old [c-r, c+r], new [c-r, c+r]); the flood sphere's centre rises by exactly r, which cannot change outdoor membership (XY rectangle) and lands the indoor half on Session B's dungeon gate alongside S1B; and the sphere-branch flood is now pinned uncapped by a genuine eleventh-shape A/B test. PublishStaticCollision — the headless static path — gains its first test ever. AP-155 is NARROWED, not deleted (review F13): the has-BSP source split (entity.MeshRefs vs setup.Parts + AnimPartChanged) survives and keeps the row active. The shared-primitive-emitter refactor that would make route independence a compile-time property is the filed follow-up (review F20). Population: 3,506 of 5,935 installed Setups, structurally equal to AP-157's third-branch count (byte-identical classifier — three independent routes agree: 3,605 - 99 = 3,506). Clean-room suite at implementation: 11,253 passed / 6 skipped / 0 failed on landed S1B. Post-review-hardening: Publisher tests 25/25, Content tests 2/2, both green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b3e43d22c9 |
fix(physics): S1B — indoor cell membership admits on the part BOX, as retail does (#335, AP-159 narrowed)
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
CellTransit.FindTransitCellsBox ports CEnvCell::find_transit_cells'
part-array overload @0x0052cae0 line-for-line: per-portal x per-part
order, the sphere cheap-reject at F_EPSILON+radius, the box admit whose
"Straddle or crossing-side" rule is exactly retail's `eax != side` under
the PDB Sidedness enum, leads-outside placed AFTER the admit, the
unconditional unloaded-neighbour hint without the sphere overload's
re-test, the destination box_intersects_cell gate with its deliberate
no-break, and add_all_outside_cells after the loop. The box-vs-cell BSP
traversal lands in BOTH representations behind the flat-authoritative
dispatcher with a graph referee whose 20,000 installed comparisons are
pinned by assertion (review F5), zero mismatch.
Dual Opus review: PASS on both lenses. The mandatory D0 pseudocode pass
caught that the contract's own supplementary note misattributed the box
block to the sphere overload — it belongs to a SECOND
check_building_transit overload @0x0052c680, whose portal-side
convention is INVERTED and whose admit differs; the pseudocode doc now
records that trap plus two byte confirmations made at review:
which_side @0x00444720 is strictly > eps for POSITIVE, and
intersect_box's in-plane early exit returns CROSSING(3)
(jp @0x005aa1bc -> mov eax,3), settling review items b1/b2 for the
future bridge porter. The bridge itself stays unported as AP-159's
explicit remainder.
The review also retired #335's severity premise honestly: "over-
inclusive only, never a missed one" is wrong at production shape ratios,
where the box (whole-vertex AABB) legitimately exceeds the sphere
(physics-polygon root sphere). Measured, both populations: rigged
(box << sphere) — 1,520 placements, 978 cells removed, 0 added;
production-ratio (box >= sphere) — 950 placements, 20 removed, 1 ADDED
through the loaded-neighbour gate, which is retail's direction, not a
defect. The no-op guard (review F4) asserts removal is nonzero so an
unwired admit cannot pass silently.
Process note: the implementer authored against this session's worktree
at
|
||
|
|
d73125d3b0 |
fix(physics): S4/AD-65 — the away-from-plane response snaps to the surface, as retail does
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
Campaign S slice S4, the half that landed. Retail's CTransition:: adjust_offset @0x0050a370 branches on dot(offset, contactPlane.N) at 0x0050a4fa: moving INTO the plane subtracts the normal component (0x0050a529), moving AWAY calls Plane::snap_to_plane @0x00509c50 — which preserves X and Y and re-solves ONLY Z so the offset lies in the plane (the d terms cancel algebraically), no-op under the 0.000199999995f |N.z| epsilon. acdream ran the orthogonal projection in BOTH directions, shrinking downhill XY travel by cos^2(theta): 25% at 30 degrees, 50% at 45 — AD-65's recorded shortfall, now retired. The combined Opus review independently re-derived the algebra, the branch polarity, the epsilon's bit-identity (17b75139), and the sabotage magnitude (the re-instated projection yields X = 0.75 = cos^2 30 exactly), and verified the delta is 4 non-comment lines with the into-plane arm, the crease arm, and both no-plane arms untouched. Its blast-radius sweep found the away arm exercised but NOT discriminated by any pre-existing test — every one asserts lower bounds the snap over-satisfies — so the two new exact-value tests are the only discriminating coverage, recorded in the test's class doc, and the felt 33-100% downhill speed-up is the morning gate's one row. AD-66 (the push-out's bare radius) is WITHHELD: byte-confirmed twice, implemented, then pulled after the same clean-room binaries measured contradictory absorbed-tick outcomes flipping with nothing but test assert shape — issue #341 carries the observation matrix and the apparatus plan; its two exact-value tests are [Skip]-ed; the retained substitution's rationale is restored at the site per review F1, with the review's remaining findings (F2/F3/F4/F5/F6) applied and F8 filed as #342. AD-69 filed: the same block omits retail's get_block_offset seam-frame correction, deferred to the AD-66 relanding for attributability. #340 filed: a fifth load-sensitive flake. Review verdict: PASS. AD-65 is provably unable to reach the #341 anomaly's code path (the absorb scenario takes the crease arm). Clean-room suite: 11,239 passed / 6 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55b07f6a62 |
refactor(physics): hoist the live-entity collision builder to Runtime (#330 groundwork)
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
LiveEntityCollisionBuilder and LiveEntityDefaultPoseResolver move from AcDream.App.Physics to AcDream.Runtime.Physics with no behaviour change — diff-verified byte-identical shape math by both review lenses. The Build signature's App-record parameter is replaced by presentation-free primitives with identical guard semantics, INCLUDING the FinalPhysicsState read the contract had missed and the implementer surfaced rather than dropped. Visibility stays internal: Runtime's existing InternalsVisibleTo grants already cover every consumer, so the implementation's public widening is reverted per the architecture review's finding 11. The registration WIRING is deliberately WITHHELD. Both Opus lenses failed it, converging: a shadow registered at spawn freezes there (RuntimeRemotePhysicsUpdater is Runtime-homed but App-driven — nothing headless ticks it), so a walking NPC becomes a phantom obstacle at its spawn point while the real NPC still passes through the bot; three of five shadow-lifetime edges leaked (pickup leaves a permanent invisible collider, supersession orphans a duplicate, generation reset never unregisters and the K-ledger convergence oracle only checks retained shadows AFTER disposal clears them); and headless cannot resolve BSP collision assets at all, so doors and chests would still be walk-through. The frozen-shadow root was the SESSION LEAD's contract error (fact 3), not the implementer's. #330 stays OPEN, rewritten as the seven-point scope map the reviews produced — the honest overnight deliverable is that map, not a half-mechanism carrying new divergences. Suite 11,235 passed / 4 skipped / 0 failed (the withheld seam's two tests account for the delta from the implementation run's 11,237). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
52aea775b9 |
test(physics): AP-157 measured — CylHeight half retired, sorting-sphere half proven collision-unreachable; AD-55 byte-decoded
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
Campaign S S1A, both outcomes the measure-first rule exists for. AP-157's CylHeight half is RETIRED as a non-divergence: retail's own cylsphere overload (CObjCell::find_cell_list @0x0052b9f0) copies localtoglobal(low_pt) + radius per cylsphere, capped at 10, and never reads height — retail collapses a cylsphere to a base-point sphere exactly as acdream does. The sorting-sphere half measured REAL against retail's registration set — 1,812 of 3,343 evaluated Setups (54%) fail containment at 1 mm, worst shortfall 18.135 m — and then PROVEN collision-unreachable: for this branch the flood spheres and the collision-test geometry are the same per-part Sphere list, so every omitted cell is one the entity's test geometry cannot reach, and retail's wider sorting-sphere registrations are narrow-phase rejects on retail too. Fix deferred to the next bake-schema revision rather than performing Slice I3 surgery for zero behavioural delta. The measurement test stays in the tree as the permanent record (population cross-checked against the dispatch test's independently-committed constants: 3,506 = 3,605 - 99). AD-55 is byte-decoded and RESOLVED against our constant: the binary loads qword [0x007c6b28] = pi/18 exactly and executes FCOS — retail's Sledding flatness threshold is cos(10 deg) = 0.984808. Our 0.99999536f is cos(0.17453 DEGREES): the radian literal misread as degrees, which makes the object-friction arm unreachable on real terrain (nothing is flatter than 0.175 deg). Evidence note carries the full instruction listing and the polarity of the test ah,0x41 / jp idiom; the one-line fix + conformance test is S5, queued behind the running implementation slice for build-slot reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
332045c7ad |
fix(physics): split set_contact_plane from init_contact_plane (#32 local edge-slide)
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
Measured live at Rithwic 2026-08-06 with ACDREAM_DUMP_EDGE_SLIDE=1. Six branch2/steep-cliffslide events, every one reporting curN=(-0.954,0.000,0.301) lastN=(-0.954,0.000,0.301) angle=0.0000 apply=False, outcome degenerate-cross/last-known. That is decision-table row 1 of the research doc, verbatim. CTransition::cliff_slide @0x0050a6d0 takes its slide direction from cross(steep contact normal, last_known_contact_plane.N) — it needs the surface the mover was STANDING ON as the second vector. acdream's CollisionInfo.SetContactPlane latched the last-known group on every call, so by the time cliff_slide ran, last-known had already been overwritten with the steep face itself: the cross product of a vector with itself, which is zero. Degenerate direction, no slide, walk off the cliff. Retail's COLLISIONINFO::set_contact_plane @0x00509d80 is 22 bytes and writes the CONTACT group only; the last-known group has four writers, none of them that function. So the four writes are DELETED and a new InitContactPlane mirrors CTransition::init_contact_plane @0x0050e850, writing both — the start-of-transition seed, where there is no earlier surface to remember. Only check_contact's SUCCESS branch calls it. The other eleven call sites keep the narrowed setter. This is a port, not a suppression: no guard, no grace period, no flag. The user's own A/B was the discriminator: Neftet's block plateaus hold (188 branch3/precipice-slide events, all before the teleport) while Rithwic's terrain cliff fails (6 branch2 events, all after). I had predicted the opposite — that terrain would be the flat-normal case — and position plus timeline corrected me, not reasoning. NEW DISCRIMINATING TEST, because the suite had none. It was green both before and after the production change, so nothing in it defended this behaviour. Issue32LastKnownContactPlaneTests seeds a walkable plane, asserts a steep mid-transition contact leaves it intact, and asserts the resulting cross product is non-degenerate. Sabotage-verified: restore the four writes and both discriminating rows fail while the InitContactPlane control keeps passing — the pair separates 'the latch is gone' from 'nothing writes last-known at all'. Two existing tests corrected rather than deleted. PhysicsSetPositionTests.FailedCheck_MapsCollisionHandlerResultToRetailError passed BECAUSE of the latch (the file the research named); its hook now populates both groups explicitly, since it asserts report plumbing, not setter semantics. RetailEdgeResponseOrderingTests.TransitionalInsert_ DegenerateCliffSlideOk_ContinuesOuterRetry was predicted to fail and did not — it now passes for a DIFFERENT reason (last-known absent rather than clobbered, which retail also answers with OK_TS). Its comment described the deleted behaviour and is corrected to say so, and to say it does not discriminate this fix. Also repairs the #338 probe. Its first placement in PlayerMovementController printed nothing across 11,523 live log lines — the wrong one of two resolve call sites — so it moves to PhysicsEngine.ResolveWithTransition where every caller passes through, filtered to the player. The dead site is removed rather than left in place; a probe that never fires is worse than none. The flag test now precedes the interpolated string: building it eagerly cost 128 B per resolve with the probe OFF, which Slice I1's zero-allocation gate caught. Suite 11,234 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ea83b043df |
fix(physics): delete the query-site broadphase reach filter (#333, closing #337)
Transition.FindObjCollisionsInCell discarded a shadow candidate when |currPos - obj.Position| > sphereRadius + obj.Radius + movement.Length() + 2f obj.Position is the part ORIGIN; obj.Radius is the physics-BSP ROOT BOUNDING SPHERE's radius, measured about a centre AP-156 established is frequently metres from that origin (376 of 973 installed physics-BSP parts sit further from their part origin than half their own radius, worst 20.762 m). Geometry deep inside the real bounding sphere was therefore thrown away before BSPQuery ever ran: solid near the origin, permeable in a bounded shell beyond it. For the Neftet rock 0xC8766009 / gfx=0x01004751 the two points are 23.556 m apart, which is #337 — wedged on the plateau, jumps sinking into the mesh, corpses falling through. A live capture recorded 7,225 rejections on that one owner, every single one with wouldAcceptAtCenter=True. Deleted rather than re-centred. Retail has no distance pre-filter, disassembled from the PDB-paired v11.4186 binary (CodeView GUID 9e847e2f-777c-4bd9-886c-22256bb87f32) rather than read from Binary Ninja: CObjCell::find_obj_collisions @0x0052b750 walks shadow_object_list and calls CPhysicsObj::FindObjCollisions (0x0052b78b) UNCONDITIONALLY; its only early-out is insert_type == INITIAL_PLACEMENT_INSERT (0x0052b759). CPhysicsObj::FindObjCollisions @0x0050f050 contains no float compare at all. CPartArray::FindObjCollisions @0x00518180 is a bare do/while over parts, and CPhysicsPart::find_obj_collisions @0x0050d8d0 is two null checks plus a call. Retail's only spatial rejection is the BSP node bounding-sphere test inside the walk — correctly centred, which is exactly what the deleted filter was not. Re-centring it (carry BoundsCenter on ShadowEntry) would have preserved an invention retail does not have, including a +2f slack and a movement.Length() term with no retail counterpart, and left a second reach budget to be tuned forever. Retail's own cross-cell slack constant is F_EPSILON = 0.0002 m, not 2 m. The method's comment claimed the filter was "the analog of the part sorting-sphere early-outs inside retail's CPhysicsObj::FindObjCollisions — response-neutral, pure perf". Both halves were false and cost #333 and #337; it is replaced by the disassembly above. Gate: Issue333BroadphaseReachFilterTests drives the production path end-to-end (ResolveWithTransition -> FindObjCollisionsInCell -> CollisionTraversal) on a DAT-free fixture so it runs everywhere, as a discriminating pair. Sabotage-verified: restore the pre-check and OffCentreBspFloorStopsAFallingMover reaches z=37.800 — exactly the unobstructed fall, blockedAtLeastOnce=False — while CentredBspFloorStopsAFallingMover keeps passing. Without the control a fixture unable to fall would pass the first test for the wrong reason. Issue337's skipped TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn asserted the now-deleted predicate and could never have gone green; it is rewritten as installed-DAT evidence pinning BOTH halves of the diagnosis and is no longer skipped. Perf measured, not assumed (Release, synthetic all-BSP cell, per ResolveWithTransition): at 38 candidates — the live maximum — 10.61 us -> 16.68 us (1.57x); at a deliberately unreachable 200, 17.34 -> 39.48 us (2.28x); ~0.16 us per additional candidate tested. Over 19,701 live [reach-q] samples the in-cell count is p50 = 9, p99 = 32, max 38. The ACDREAM_PROBE_REACH rejectedReach column is kept and is now structurally 0, so a post-fix capture stays comparable with the pre-fix one; dropping it would make the two incomparable. AP-158 retired (110 active AP rows). #333 and #337 closed pending the user's live acceptance at Neftet. Solution suite 11,231 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5a1eeace73 |
docs(physics): #337 diagnosed — it is #333's query-site broadphase, not the mesh
Report-only. No production code changed. The collision mesh is present, correctly shaped, correctly placed in the world, and the BSP traversal reaches every part of it. The mover never gets as far as the query. FindObjCollisionsInCell's per-object broadphase measures the mover's distance to the shadow entry's Position — the part ORIGIN — and compares it against obj.Radius, which is the physics-BSP ROOT BOUNDING SPHERE's radius. For 0xC8766009 those two points are 23.556 m apart, so a mover standing on its plateau is inside the real bounding sphere by ~20 m of margin and is still rejected. Same defect AP-156 fixed in the flood and #334 fixed in the registration extent walk, left in place at the query site. Measured, not inferred. An offline replay against the installed DAT reconstructs all eleven landblock-0x8766 owners and matches the live [geom] placement exactly (0xC8766002 at (84.699,100.082,13.000) yaw -45.00 vs the log's objPos + bspCentreOffset). At the position the client fell through, the production swept query returns a hit on poly 31 at 0.037-0.366 m while the filter rejects the candidate: distToOrigin=60.434 > maxReach=59.697, distance to the bounding-sphere CENTRE 37.083 m against a 56.909 m radius. The live capture recorded that rejection 7,225 times with the probe's own wouldAcceptAtCenter=True on every one. Bounded because the dead zone is the shell between maxReach and the true sphere, up to ~23.5 m thick on the far side. movement.Length() is a budget term: a 0.25 m walking step gives shortfall +0.60, a 0.72 m step +0.14, and ~0.86 m passes — which is exactly why jumping over the spot works, walking into it does not, and a corpse falls through. Three hypotheses refuted by measurement, not by argument: - "the rock's own mesh never collides" — true of 0xC8766002 and it is INNOCENT; its geometry is 22.8 m from the wedge and it has zero brute-force hits over a 12,493-point lattice covering the plateau. It is a candidate only because it is a 130x147 m owner. The rock actually walked on is 0xC8766009. - wrong world transform — the offline placement reproduces the runtime exactly, and a uniform displacement cannot produce a bounded pocket. - BSP traversal hole — a referee ran the production walk against brute force at 7,770 on-surface probes across all eleven owners plus 137,423 lattice points. Mismatch 0 everywhere. A 0.5 m hole map also shows continuous upward-facing coverage across the whole wedge region. [geom]'s verdict=coincident was never able to decide this: LogGeometry compares the physics box against the visual box in the object's OWN LOCAL FRAME, so it proves shape agreement and says nothing about world placement. Recorded in the doc so the next reader does not re-trust it. Retail has no per-object distance filter on the BSP branch. Verified instruction-by-instruction with cdb against the PDB-paired v11.4186 binary: CPartArray::FindObjCollisions @0x00518180 is 14 instructions of bare do/while over parts[i]; CPhysicsPart::find_obj_collisions @0x0050d8d0 is 17 instructions of two null checks plus the call to CGfxObj::find_obj_collisions @0x00534700. No compare, no float math in either. The in-tree comment calling the filter a retail analog and response-neutral is wrong on both counts. The support=object cpNz=1.0000 readings inside the rock are not the rock: ValidateTransition:6076 is retail's stationary-fall failsafe manufacturing a flat plane through the sphere bottom, and :5997 is the LastKnownContactPlane restore holding a stale plane. Both are retail-correct responses to a stuck body, and they are why the client believes it is standing while ACE rejects the position. Preferred fix is to delete the pre-check for BSP entries and correct the comment; fallback is to measure to the bounding-sphere centre, which also needs BoundsCenter carried on ShadowEntry. Neither is landed. The reproducer was confirmed to FAIL when un-skipped, with the numbers above — this campaign has caught eleven green tests covering nothing, so a fixture that cannot distinguish the bug is worse than none. Gates: bin/obj deleted, Release build 0 errors, Core suite 4,287 passed / 2 skipped / 0 failed (baseline 4,286/1 plus three new dumps and the one deliberately skipped reproducer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
49a7e90652 |
probe(physics): ACDREAM_PROBE_SUPPORT + ACDREAM_WIRE_MESH — separate #337's three candidates
The user is wedged at the top of Neftet rock plateaus, jumps sink into the mesh, and a corpse falls straight through. ACDREAM_PROBE_REACH already ruled out its own domain: blocked=0, every candidate tested-ok. Three candidates remain — terrain support, a collision mesh not where its visual is, or the transition wedging on an unobstructed path. ACDREAM_PROBE_RESOLVE alone cannot separate them. It prints a three-value contact-plane token, no plane normal, no plane height, no terrain sample and no plane provenance, so all three produce the same line. Two additions: [support] — one line per resolve for EVERY body, not just the player. A corpse is a plain physics body with no player-specific logic, so its fall-through is the cheapest available control on "movement code vs geometry data", and it is invisible to any player-filtered probe. The line samples the outdoor terrain INDEPENDENTLY at the body's own out-XY and prints the contact plane's own height at that same XY. Two heights at one point make support=terrain / object / none a measurement rather than an inference, and cpSrc= names the site that asserted the plane so provenance and classification cross-check. [geom] — once per GfxObj that comes near a mover: the object's physics-BSP vertex cloud against its visual mesh AABB in the same local frame, through the same prepared accessors the resolver queries. verdict=coincident REFUTES the working hypothesis for that object outright; no-physics-bsp / empty-physics-bsp / displaced / extent-mismatch each name a specific data defect. Built to refute, not to confirm — two diagnoses on this defect's lineage have already been refuted by measurement. ACDREAM_WIRE_MESH upgrades the existing F2 overlay, which drew a broadphase proxy cylinder for BSP objects and so could not answer the question at all, to the real physics-BSP polygon edges (cyan) beside the visual mesh box (magenta) and the terrain surface (yellow). Own class per code-structure rule 1. The provenance latch lives on PhysicsDiagnostics, not on CollisionInfo. Two fields there first — the obvious home — broke the flat/graph differential referee and the scratch-reset poison test, both of which compare CollisionInfo member-for-member. Teaching either to skip a member is a one-line green fix that puts a permanent hole in a referee whose whole job is comparing everything. Captured as feedback_probe_state_off_compared_types. Seven tests cover the support classifier's boundaries: a wrong classifier does not fail to answer, it answers confidently wrong. Gates: Release build 0 errors; complete suite 11,225 passed / 4 skipped / 0 failed from a cleaned tree — baseline 11,218/4/0 plus exactly the seven new tests, skips unchanged. Issue #337 filed with the symptom set, what is ruled out, and a table of what each possible output means. All of this is TEMPORARY and recorded for stripping with the physics-probe family. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
13fcf38138 |
fix(physics): port retail's find_bbox_cell_list outdoor extent walk (#334)
acdream had never implemented retail's SECOND cell-membership algorithm.
CPhysicsObj::calc_cross_cells @0x00515230 tests HAS_PHYSICS_BSP_PS at
0x00515285 and jumps (0x0051528f jne 0x515305) to find_bbox_cell_list
@0x00510fc0 for a BSP-bearing object; everything below that jump is the
OTHER algorithm, CObjCell::find_cell_list, and that is all we had. Every
object, BSP-bearing or not, was routed through it.
That path's outdoor expansion is a HARD CAP of one cell in each direction.
CellTransit.AddAllOutsideCells computes minRad = radius, maxRad = 24 - radius
and adds at most the eight neighbours of the sphere's own cell, so for any
radius >= 12 m both boundary tests are unconditionally true and the result is
exactly 3x3. Widening the radius or adding a second sphere is mechanically
incapable of adding a tenth cell. The user's live probe measured the
consequence directly: standing inside a Neftet formation, inCell=2 exempt=2
reached=0 -- the geometry was not a candidate at all.
The port. AddAllOutsideCellsFromParts is CLandCell::add_all_outside_cells
@0x00533360 plus add_cell_block @0x005331d0: base landcell from the FIRST
part's own adjust_to_outside, baseX/baseY within-block, each part's authored
CGfxObj::gfx_bound_box re-fit through all eight corners
(BBox::LocalToGlobal @0x005b2120), floor(v / square_length) where
square_length = 0x7c920c = 24.0f, four accumulators seeded to zero, ONE
rectangle unioned across all parts, FILLED, in GLOBAL lcoords so it crosses
landblocks freely, clamped only to [0, 0x7f8).
BuildShadowCellSetFromParts is find_bbox_cell_list's worklist.
RegisterMultiPart dispatches on the same flag retail does, and
BuildFloodSpheres' BSP arm is deleted rather than left unreachable.
Disassembled from the PDB-paired 2013-09-06 binary, not read from Binary
Ninja: BN mis-renders four separate constructs inside add_all_outside_cells
alone -- a dropped `and eax,0xffff` on baseX, a neg/sbb/and select shown as
identically zero, a wrong get_landcell argument, and both x87 flag tests as
`unimplemented {test ah}`.
ShadowPartGeometry pairs the BSP root sphere with the authored box so no
resolver can answer one and leave the other call site to synthesize a
substitute -- the AP-156 invariant applied a second time, since that split is
what produced AP-156 and then this. The box comes from
FlatGfxObjVisualBounds, already computed by exactly CGfxObj::init_end's
algorithm and already in the prepared package: no bake change, no DAT re-read.
Cost, measured over the installed DATs before any code was written: 1,258
physics-BSP GfxObjs, cells/object p50 4, p90 4, p99 12, max 49. The port is
CHEAPER than the old 3x3 = 9 for 98.97% of them. Row totals (shapes x cells)
over all 1,031 landblocks with BSP owners fall 97,173 -> 15,607 (0.161x);
dense Arwic 0xC6A9 falls 342 -> 43. One landblock more than doubles.
Precondition confirmed before pinning any expected cell set: 0x010046D8's box
is 96 m x 96 m about cell (2,2) = 0x87640013, which independently corroborates
the 3x3-centred-there diagnosis, and its rectangle does contain 0x87640011 and
0x87640019 -- the two cells the probe measured empty.
Register: AP-156's outdoor half CLOSED and its risk column CORRECTED (it read
"extra broadphase candidates, never a missed one", which generalised the indoor
direction to the whole row and is why #334 sat inside it unnoticed). AP-159 +
issue #335 file the unported indoor arm; AD-49 records the seed-time rectangle.
Issue #336 files a fourth load-sensitive test flake seen once during the gate.
Ten tests, every one sabotage-verified in both directions across eight
mutations (dispatch, 8-corner refit, floor-vs-truncation, union-vs-per-part,
map clamp, adjust guard, landblock clamp, box-path-for-everything). The
strongest is an installed-DAT replay of the user's own probe evidence.
Suite 11,208 -> 11,218 passed / 4 skipped / 0 failed; the +10 is exactly the
new tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e6457cc849 |
fix(physics): close the AP-156 fix review — real containment oracle, type-level invariant, AP-158
Both review lenses PASSED; this is the cleanup, not a rescue. Evidence:
docs/research/2026-08-06-ap156-review-closure.md (the review itself is
committed alongside it as the received artifact).
R1 — the load-bearing containment test could not fail. Its truth and flood
values were two hand-copies of the same expression over the same part set,
so the shortfall was algebraically identically zero for any DAT input. The
oracle is now PHYSICS-POLYGON VERTICES — a different DAT field from the
bounding sphere the builder emits, so the two sides can genuinely disagree.
Sabotage-verified three ways after full cleans: dropping the bounds centre
in production reddens it (428 Setups, worst 35.869 m on 0x0200129A, matching
an independent out-of-repo sweep exactly); dropping only the scale on the
centre reddens it (326); and corrupting the TEST's own bounds oracle reddens
it (467) where under the shipped oracle that same corruption was invisible
by algebra. Renamed accordingly. A6's stale "cap control" comment corrected:
that loop is the test's own uncapped re-implementation and cannot observe a
cap regression — the cap is covered in Core.
R2 — the population was understated. 172 is AP-152's DISPATCH population;
AP-156's is 530 BSP-bearing Setups, of which 525 have a flood sphere move
and 428 fail vertex containment before the fix (412 at a 1 cm tolerance —
the review's figure; the gap is 16 Setups between 1.4 mm and 10 mm, real
geometry). 0 fail after, at any tolerance down to zero. Corrected in the
AP-156 row, the section-3 header, the C5c handoff and two test docstrings.
Dated review artifacts are left as written — "170 of 172" was correct for
what they measured, and rewriting evidence to match a later measurement
loses provenance.
A1 — BoundsCenter = default reopened at the type what the commit closed at
the seam. Dropping the default alone would NOT have closed the review's own
scenario (a copied Cylinder call site would write Vector3.Zero explicitly
and stay green), so ShadowShape's constructor is now private and BSP shapes
are built only through ShadowShape.Bsp(..., FlatCollisionSphere localBounds),
which takes radius and centre as ONE value and scales them together. There
is no expression a caller can write that carries one and drops the other.
22 construction sites converted; the same sabotage now reddens 5 Core tests
where the review's sabotage A reached 4, because both BSP producers share
one scaling path.
A2 — #333 is real and bigger than filed, and its retail question is
answered. I disassembled CObjCell::find_obj_collisions @0x0052b750 from the
PDB-paired binary myself (check_exe_pdb.py MATCH) rather than inheriting the
claim: its only early-out is sphere_path.insert_type == INITIAL_PLACEMENT_
INSERT, then it calls FindObjCollisions on every unparented non-self shadow
object UNCONDITIONALLY. Retail has NO distance pre-filter, so acdream's
"+ movement + 2f" reach filter is an invention with no register row — filed
as AP-158, carrying the disassembly, the F_EPSILON = 0.0002 m contrast, and
the measured blast radius (118 of 477 unique installed physics-BSP GfxObjs
exceed its ~2.5 m budget, 46 exceed 5 m). Active AP rows 109 -> 110.
Recorded prominently in three places a reader will hit: TALL PROPS MAY SHOW
NO VISIBLE CHANGE UNTIL #333 LANDS, and a null result at the connected gate
is EXPECTED, not evidence against AP-156.
LOW items. R3: the comment claiming the cited evidence justified the whole
cap line is corrected, but int.MaxValue on the sorting-sphere branch stays —
capping at 1 would take Spheres[0], and retail's one sphere is
CSetup::sorting_sphere, a different DAT field; capping keeps the wrong field
AND flips the substitution under-inclusive (#98/#168 direction). AP-157
already owns it. R4: acdream scales the flood sphere where retail's
find_transit_cells never reads gfxobj_scale — added as a second residual on
AP-156. R5: retail's slack constant carried into AP-158 and #333. A3: the
per-call delegate allocation is back to a cached field, still derived from
the single bounds resolver. A5: noted; b52967de's message cannot be amended.
Gates: all 44 bin/obj deleted before every verdict-deciding build, each test
run gated on a verified "Build succeeded" in the same invocation. Release
build 0 errors / 21 pre-existing warnings. Complete suite 11,208 passed /
4 skipped / 0 failed — reconciles exactly with the
|
||
|
|
b52967def3 |
fix(physics): AP-156 — flood the BSP sphere where the geometry is, not at the part origin
The AP-152 retail review (docs/research/2026-08-06-ap152-review-retail.md) FAILED ` |
||
|
|
4abd1b5eb7 |
fix(physics): AP-152 — dispatch collision shapes BSP-first, at emission and at the cell flood
The register row predicted "catching or stopping on a doorway sill". That
symptom could not have been occurring. `Transition.BspOnlyDispatch`
(TransitionTypes.cs:1348, landed 2026-05-25 as A6.P7) already skipped both
primitive branches (:3911, :3954) whenever the target's wire PhysicsState
carries HAS_PHYSICS_BSP_PS, and ACE sets that bit from CSetup.HasPhysicsBSP
for every affected Setup. The extra primitive was never tested for collision.
The live defect was CELL MEMBERSHIP. The same shape list feeds
`ShadowObjectRegistry.BuildFloodSpheres`, which had no such guard and
preferred Cylinders over everything whenever any Cylinder existed — retail's
SECOND priority applied ahead of its first. For the 73 CylSphere+BSP Setups
acdream therefore flooded shadow cells from the cylinder and never from the
slab: an object absent from cells it physically occupies, which is the
#98 / #168 symptom class, not the door-collision class the row named.
Retail, re-disassembled from the PDB-paired binary (v11.4186, CodeView GUID
9e847e2f-777c-4bd9-886c-22256bb87f32, check_exe_pdb.py MATCH) rather than
taken from Binary Ninja, which drops flag tests:
CPhysicsObj::FindObjCollisions @0x0050f050
0x0050f165 test dword [esi+0xa8], 0x10000
0x0050f16f je 0x50f1a2 ; clear -> primitive dispatch
0x0050f18d call 0x518180 ; CPartArray::FindObjCollisions
0x0050f19d jmp 0x50f2b0 ; UNCONDITIONAL, past BOTH primitive loops
; (CylSphere 0x50f1a2, Sphere 0x50f21d)
0x0050f1d6 jae 0x50f317 ; CylSphere loop exhausted -> RETURN
0x0050f22f je 0x50f31b ; zero Spheres -> RETURN seeded OK_TS
CPhysicsObj::calc_cross_cells @0x00515230
0x00515285 test dword [esi+0xa8], 0x10000
0x0051528f jne 0x515305 -> CPhysicsObj::find_bbox_cell_list @0x00510fc0
0x005152d1 call 0x52b9f0 ; cylsphere branch, below the jump
0x005152fb call 0x52b990 ; sorting-sphere branch, below the jump
Priority at both consumers: BSP -> CylSphere -> Sphere -> nothing. BSP wins.
Every address above was resolved back to its symbol by exact lookup in
named-retail/symbols.json.
Changes:
* `ShadowShapeBuilder.FromSetup` gains a step-0 dispatch gate. Steps 1 and 2
are skipped entirely when any part's EFFECTIVE GfxObj carries a physics
BSP. The gate and step 3 now share one `EffectivePartGfxObjId` helper, so
they cannot read different identities — a gate on `setup.Parts` would,
after an ObjDesc swap, suppress the primitives while step 3 emitted
nothing and `Build` returned null, deleting the entity's collision.
Emission order is unchanged. This also removes acdream's undeclared
reliance on the server sending the flag: the gate is derived from the
parts, exactly as CPartArray::CacheHasPhysicsBSP @0x00518110 derives it.
* `ShadowObjectRegistry.BuildFloodSpheres` now applies calc_cross_cells'
own order: BSP, else Cylinder, else everything. Given the gate above this
is a no-op for every shape list acdream produces (FromSetup is now
exclusive; both landblock-static publishers already emit homogeneous
lists), so the measured membership delta remains attributable to the
gate alone. It is kept for the same reason BspOnlyDispatch is kept: retail
genuinely dispatches here, and it guards a future additive producer.
`Transition.BspOnlyDispatch` is deliberately untouched.
Register: AP-152 RETIRED with its four false statements corrected — the risk
statement (the symptom was already inert); "small and centred at the part
origin" (max primitive is 6.714 m, and 0x0200086E's sphere origin is
(0.759, 0.165, 5.842)); the cottage door's "~14 cm base Sphere" (it is
0.100 m; 0.141 is Setup.Radius, which AP-22 proved is never collision
geometry); and naming one pinning test where two existed. AP-153/154/155
filed: retail's dispatch flag is cached once at InitPartArrayObject+0x7e
where acdream's gate is live; the query-time guard takes a client-derived
flag off the wire; and the static publishers emit Setup Spheres as
height-capped Cylinders while BuildFloodSpheres approximates retail's
bounding box with bounding spheres.
Tests. Both pinning tests corrected, neither deleted:
`FromSetup_DoorSetup_ProducesFourShapes` -> `..._EmitsBspPartsOnly`;
`FromSetup_DoorSetup_SphereAtExpectedLocalOffset` re-hosted on
`_ => false`, the DAT-real configuration for the 3,605 Sphere-only Setups.
`FromSetup_ScaleFactor_MultipliesAllRadiiAndOffsets` was the campaign's
eighth green test covering nothing — its assertions sat inside
`if (CollisionType == Cylinder)` on a fixture with zero CylSpheres, so only
`Scale == 2.0f` ever ran. Proved empirically: with the sphere radius scale
deleted, the old body passes and the corrected body fails. Three new facts:
the effective-identity gate, the App-layer CylSphere+BSP registration (no
App fixture combined the two before), and the flood-set dispatch. One new
installed-DAT sweep pins 172 affected Setups (73 CylSphere+BSP, 99
Sphere+BSP) behind external bucket controls, re-measured independently and
agreeing exactly with the filing commit's separate sweep.
All eight sabotages run and reported; every discriminating fact reddens in
the intended direction and only there. Clean Release build after deleting
every bin/obj: 0 errors. Complete suite 11,203 passed / 4 skipped / 0
failed, +5 on the 11,198 baseline at
|
||
|
|
ec29a732f5 |
test(physics): settle #331 — the uphill "refusal" is the #137 anti-parallel absorb, not a defect
#331 reported that `PhysicsEngine.ResolveWithTransition` refuses ALL uphill
motion whenever a `body:` is supplied. It does not. It refuses a step whose
sub-step offset is exactly anti-parallel to a live sliding normal — the
#137-family absorb this project already recorded as retail-faithful.
Measured on the same fixture, same gradient, same body, varying only the
heading relative to the slope gradient:
(0, -0.1, 0) cross-slope 0 -> zero movement, latched
(0.0001,-0.1, 0) cross-slope 0.0001 m -> zero movement, latched
(0.001, -0.1, 0) cross-slope 0.001 m -> climbs 0.176 m in 5 ticks
(0.01, -0.1, 0) cross-slope 0.01 m -> climbs 0.176 m in 5 ticks
The threshold is retail's own F_EPSILON small-offset abort (0.0002 m): about
0.11 degrees off the exact gradient at a 0.1 m step. `RemoteRampHarness`
builds a ramp whose gradient is exactly along Y and the original probe pushed
exactly along -Y, so it hit the measure-zero case with probability 1.
The latch itself is production-real in mechanism — a pure gravity fall under
the production RuntimeRemotePhysicsUpdater, with no fixture settle seam
involved, lands leaving Contact|OnWalkable|Sliding with slidingNormal (0,1,0)
— but every link is faithful to retail, verified in the PDB-paired binary
rather than Binary Ninja (BN typed find_transitional_position `void` and
dropped the load-bearing return value):
validate_walkable sets collision_normal from the terrain plane when
OBJECTINFO CONTACT is clear 0x0050d251 / 0x0050d261 / 0x0050d26c
validate_transition converts it unconditionally 0x0050ac19-0x0050ac30
set_sliding_normal zeroes Z AND re-normalizes 0x0050a060
SetPositionInternal persists SLIDING_TS 0x005154c2 / 0x005154e1
get_object_info re-seeds it next frame 0x00511d44 / 0x00511d4f
find_transitional_position returns
`i != 0 && state == OK` on the step-0 abort 0x0050c0ed -> 0x0050c089
ACE agrees (Transition.cs:1027, CollisionInfo.cs:58). No production code
changed; no divergence introduced, so no register row.
What lands is the coverage whose absence made this invisible — nothing in the
suite asserted that a body-bearing mover makes uphill progress on a walkable
slope, and the test that found #331 passed vacuously because the body never
moved:
RuntimeRemoteUphillProgressTests.ARemoteWithABodyClimbsAWalkableSlopeAndKeepsItsFeetOnIt
per-tick climb + surface tracking under a realistic off-gradient heading.
SAB-A1 AdjustOffset -> Vector3.Zero reddens at tick 1
SAB-A2 fixture gradient -> 0 (flat) reddens at tick 1
RuntimeRemoteUphillProgressTests.AnExactlyUpSlopeOffsetIsAbsorbedByThePersistedSlidingNormal
characterization pin for the absorb, with the retail anchors inline.
SAB-B1 delete the get_object_info sliding seed reddens (climbs to 57.7544)
SAB-A1 reddens
SAB-A2 reddens
NON-discriminating, measured and documented: making the final tick
exactly up-slope leaves it green — by then the latch is already cleared.
RemoteRampHarness gains a warning block naming the axis-alignment trap so the
next vacuous uphill assertion is caught at authoring time.
Suite re-measured from a full clean (43 bin/obj removed): 11,198 passed /
4 skipped / 0 failed, against the 11,196/4/0 baseline at
|
||
|
|
886333a2a9 |
refactor(physics): delete the redundant pre-sweep slope projection (AD-10 retired)
Stage 0's measurement (previous commit) says the projection is redundant,
so AD-10 retires by deletion rather than by narrowing.
The measurement. With the sample forced to null at BOTH fork sites, from a
clean build:
* a remote running 30 ticks down a 31-degree walkable ramp produces a
BIT-IDENTICAL trajectory, position for position;
* on an 8.4-degree ramp the two differ by at most 2.8e-5 m in Z after 30
ticks (0.03 mm) and are identical in X and Y — float ordering noise
from projecting twice against the same plane rather than once;
* the whole AcDream.Runtime.Tests suite is unchanged.
That is what redundancy looks like, and the arithmetic explains it. The
boundary projection and Transition.AdjustOffset are the same operation
(v -= N * dot(v, N)) against the same plane, and the composition is
idempotent: a vector already on the plane has dot(v, N) == 0, so the
sweep's own projection is a no-op on an already-projected offset and the
full-strength projection on an unprojected one. Either alone produces the
same offset. On terrain a THIRD mechanism, ValidateWalkable's push-out,
re-seats the sphere on the plane every sub-step regardless.
Deleted:
* both RuntimeRemotePhysicsUpdater sample sites (the host and no-host
fork branches carried the block verbatim — the AP-22 shape, a row
naming one site where two exist);
* the terrainNormal parameter and projection block on
RemoteMotionCombiner.ComposeOffset;
* the same block on ComputeOffset, which has no production callers but
held a second copy of the divergence, so leaving it would have made
the row's retirement false;
* PhysicsEngine.SampleTerrainNormal, now callerless.
Removing the parameter rather than passing null is deliberate: it is what
makes a future one-site-only regression a compile error instead of a
silent half-fix.
Two tests went with it —
ComputeOffset_RootMotionFallback_SlopedTerrainNormal_ProjectsZOntoSlope and
its flat-ground twin. Both were weak on their own terms: they drove the
production-dead ComputeOffset and computed their expected values by
re-implementing the projection formula, so they could catch a wrong
MULTIPLY but never a wrong PLANE — which is exactly what the divergence
was. The surviving coverage is geometric and runs the production tick.
Three claims in the old row did not survive contact with the code and are
recorded in the retired row rather than quietly dropped: the justification
(remotes do run the sweep); the description of ComposeOffset's guard as
"interpolation-active" when the code reads `if (!interpolationOverwrote`;
and the roof clause, stale since Bug B gated the sample on OnWalkable —
a steep roof is OnWalkable == false, so the path never ran on #32's
geometry. The retail anchor is corrected too: pc:272296-272346 truncated
both the sliding-normal validity gate at the head and the entire safety
push-out block at the tail. The whole function is 0x0050a370,
pc:272271-272393.
This does not fix #32 and does not partially fix it. #32's remote half was
already closed at
|
||
|
|
fe6ee877d1 |
test(physics): measure whether the remote sweep alone tracks surface Z (AD-10 Stage 0)
AD-10 claims the remote slope projection is "relocated" out of the sweep
because "remote bodies don't run a full local transition sweep". That
justification is false at HEAD: RuntimeRemotePhysicsUpdater.Tick calls
PhysicsEngine.ResolveWithTransition with the remote's own body, and that
sweep runs acdream's verbatim port of CTransition::adjust_offset
(0x0050a370, pc:272271-272393) once per sub-step. So the boundary
projection is an EXTRA layer, not a relocation — and whether it is doing
anything the sweep does not is a measurement, never an argument.
This commit builds the fixture for that measurement and changes no
production code.
RuntimeRemoteSteepContactSlideTests' private Harness is extracted to
RemoteRampHarness so the new tests share it instead of cloning ~180 lines.
The extraction is behaviour-preserving; its only additions are the
fixture's own TerrainSurface (so an assertion about "is the body on the
surface" is answered by the surface geometry rather than by
re-implementing what the code under test computed), a SurfaceZ helper, and
a Tick overload that supplies a per-frame body-local root displacement —
the locomotion-cycle push a running remote actually carries. All ten Bug B
tests pass unchanged against it.
RuntimeRemoteSlopeProjectionTests then drives the production tick 30 ticks
down a 31-degree walkable ramp and asserts, on EVERY tick rather than at
the end, that the body's root stays within 5 mm of its settled offset from
the terrain beneath it. A staircase catching up on the final tick would
pass a start/end comparison; 30 unprojected ticks accumulate ~1.8 m.
Sabotage results, all from clean builds (bin/obj deleted), reported in
both directions:
* Discard the sweep's answer (Body.Position = postIntegratePos instead
of resolveResult.Position): RED at tick 1, body 0.05999 m off the
surface. This is the tracking test's discriminating sabotage.
* Flatten the ramp to gradient 0: RED on the anti-vacuity guard
(dz = 0.0000 m). That guard exists because the tracking assertion
passes trivially on flat ground, where Z never has to move.
* Short-circuit Transition.AdjustOffset to `return offset;`: GREEN.
Recorded, not hidden — it is the reason the contract's proposed T1
sabotage was rejected. On terrain the sweep has a SECOND independent
way to plant Z: ValidateWalkable's push-out re-seats the sphere at its
natural resting distance from the terrain plane every sub-step.
Removing the step-down probe as well does not change it either
(measured). The tests therefore assert the OUTCOME the projection
exists for, and say in their own doc comments that they are not unit
tests of adjust_offset and must not be cited as such.
One test the contract asked for is deliberately absent. An uphill
counterpart was written, passed, and was then found VACUOUS: on this
fixture ResolveWithTransition returns ok=False for uphill motion and the
body does not move at all, so it "tracked the surface" by standing still.
That finding is filed separately rather than shipped as a green test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
619de97ad1 |
fix(test): evaluate BOTH deleted guards; correct AP-22's overstated coverage claim
From the AP-22 dual review (both lenses PASS). No production change.
THE RECORD WAS WRONG.
|
||
|
|
bc4679cda5 |
fix(physics): delete the invented Setup-radius collision cylinder (AP-22)
Retail synthesizes NO shape for a shapeless object, so the fix is deletion,
not a corrected height formula.
CPhysicsObj::FindObjCollisions @0x0050f050 dispatches exclusively -- BSP xor
CylSphere xor Sphere xor nothing. The BSP branch leaves via an unconditional
`jmp 0x50f2b0` at 0x0050f19d and cannot reach the primitive branches; a
CylSphere-bearing object that survives its loop returns rather than falling
through to the Sphere loop; and with zero cylspheres, zero spheres and no
physics BSP, `0x0050f22f je 0x50f31b` branches straight to the epilogue,
returning the OK_TS seeded at `0x0050f13b mov edi,1`. CPartArray::GetRadius
(0x005180a0) and GetHeight (0x005180b0) are absent from the function's entire
call set -- Setup.Radius/Height serve attack cones, cylinder_distance and
MoveTo, never collision geometry. Disassembled directly from the PDB-paired
binary (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32) rather than read from the
Binary Ninja text, whose ebp_1 aliasing in this function is visibly corrupt.
THREE copies were deleted, not one. The AP-22 register row cited
LiveEntityCollisionBuilder.cs and ShadowShapeBuilder.cs; the latter never
reads Setup.Radius at all, and the row omitted both
LandblockPhysicsPublisher.PublishStaticEntity and
LandblockPhysicsContentBuilder.PublishStaticCollision -- the second being the
only copy the headless host executes. Fixing just the cited site would have
left headless statics on the invented footprint.
The branch was unreachable dead code, not a live approximation. A sweep of all
5,935 Setups in the installed client_portal.dat -- validated by byte
accounting (5,935/5,935 records consumed with an exact 20 + 48*numLights
residual tail, zero unexplained bytes) and independently reproduced by the
production FlatCollisionAssetBuilder.FlattenSetup path -- finds 0 Setups
satisfying the guard: every Setup with Radius > 0.0001 carries at least one
CylSphere or Sphere, and all 1,294 genuinely shapeless Setups have Radius
exactly 0. Buckets: 678 cylsphere, 3,605 sphere-only, 358 BSP-only, 1,294
shapeless, 4,282 with Radius > 0.0001. Nothing loses collision because nothing
gained it, so no visual gate is required.
Tests, all sabotage-verified in both directions:
- InstalledSetupCollisionReachabilityTests (new, Content) -- the negative
claim plus five EXTERNAL positive controls, so a broken enumeration cannot
satisfy it vacuously. Inverting the claim reddens it; emptying the
enumeration fails on the controls at 0 != 5935 rather than passing.
- ShapelessSetupWithRadius_ProducesNoRegistration (new, App) -- restoring the
deleted block reddens exactly this fact and nothing else.
- Build_PropagatesExactStateFlagsScaleAndFullSeedCell -- re-hosts the state /
PWD-flag / seed-cell coverage that rode on the deleted fallback test, whose
fixture (a Setup with a radius and no primitives) cannot exist in the DAT.
Flipping a FromPwdBitfield bit reddens it; so does swapping SeedCellId for
the landblock id.
Also corrects ShadowShapeBuilder's retail-anchor comment, which claimed each
part's find_obj_collisions tests "CylSpheres + GfxObj BSP".
CPhysicsPart::find_obj_collisions @0x0050d8d0 tests ONLY the GfxObj physics
BSP; CylSpheres are a Setup-level array reached via CPartArray::GetCylsphere.
That comment was the written justification for the additive emission now filed
as AP-152, so it is corrected here even though AP-152 is not fixed here.
AP-22 retired with evidence; AP-152 filed (live path emits primitives AND BSP
parts additively where retail is exclusive -- 172 of 5,935 Setups including
BSP doors; deliberately not folded in, it needs its own visual gate). Issue
#330 filed: the headless host registers no live-entity collision at all, a
pre-existing gap this survey established and nothing tracked.
Gates: Release build 0 errors / 0 warnings. Complete solution suite
11,195 passed / 4 skipped / 0 failed (baseline 11,193/4/0 at
|
||
|
|
bcb66ccdf3 |
fix(test): cover the atlas-tier seam the D-1 fix depends on; correct AP-150's citation
Both items come from the D-1 fix review (both lenses PASS, D-1 genuinely closed). No production behaviour changes. L1 — THE SEAM HAD NO COVERAGE. The D-1 fix's "empty by construction" claim rests on LandblockSpawnAdapter's atlas-tier filter (`if (entity.ServerGuid != 0) continue;`) skipping the live server projections DetachNearLayer deliberately RETAINS across a demote. The reviewer removed that filter and all 4,170 App tests passed — only two Core unit tests caught it, none through a demote. So the invariant the re-assert depends on could have been deleted silently, re-opening D-1 by another route: a non-empty re-assert whose mesh reference is never satisfied leaves IsRenderReady false, which is the portal hang again. NearToFarDemote_WithALiveServerEntity_StaysRenderReady now demotes a landblock that CARRIES a live server-spawned entity through the real GpuWorldState + LandblockSpawnAdapter + LandblockPresentationPipeline, and asserts the retained entity never enters the desired set. Sabotage-verified: with the filter removed, exactly one test fails — this one — and the other 25 pass, including all four D-1 regression tests. That is the finding restated as a measurement: the D-1 tests genuinely do not cover this seam, and now something does. AP-150 citation corrected: the row cited 0x004D7064 as the ECM_UI::SendNotice_DisplayStringInfo call site. That address is the PStringBase construction of the "In Portal Space - Please Wait..." literal (:219516); the actual call is 0x004D70A1 (-> 0x006925B0). Same class of slip the #280 commit had just corrected for #326 — worth noting that a row filed WITH a byte-level disassembly still mis-cited a neighbouring address. Also refactored the existing pipeline demote test to keep its doc comment attached to its own method (an earlier insertion had orphaned its [Fact]). App.Tests 4,170 -> 4,171 passed / 3 skipped, net +1 for the new test. No new skips; none of #302/#308/#321 surfaced. src/ is byte-unchanged (the sabotage was reverted and verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
73cdb95c7b |
fix(streaming): make a demoted landblock render-ready like a published one (#280 D-1)
Both #280 review lenses returned FAIL on the same defect, and both were
right. IsRenderNeighborhoodResident's widened outer arm requires
IsRenderReady out to FarRadius, justified by "a Far-tier landblock
registers with an empty mesh set and is therefore render-ready." That held
only for a landblock that ARRIVED as Far. The second, equally first-class
way to be Far tier is a Near->Far DEMOTE:
DemoteLandblock -> EnqueueNearLayerRetirement
-> LandblockRetirementStage.MeshReferences
-> GpuWorldState.ReleaseLandblockMeshReferences
-> LandblockSpawnAdapter.OnLandblockUnloaded => WantsLoaded = false
while DetachNearLayer deliberately keeps the landblock loaded, terrain-mesh
resident, terrain-collision resident and DRAWN. Nothing re-publishes an
already-loaded landblock, so the demoted member satisfied NEITHER arm of
the gate, permanently: wormhole tunnel plus centered "In Portal Space -
Please Wait..." forever, no recovery short of relog.
Reachable by ordinary play. Two consecutive recalls to the same landblock
with walking in between makes ChangesStreamingCenter false, so there is no
origin recenter and the region recentres through the ordinary demote diff.
Also reachable via a mid-hold quality-preset drop -- ironically the exact
scenario ReconcileDestinationReservationRadius was added to support. The
pre-#280 radius-1 gate never touched that band, because nothing inside the
Near ring can demote.
FIX SHAPE. Make the two routes genuinely equivalent rather than teaching
the predicate to tolerate the difference. ReleaseLandblockMeshReferences
becomes "reconcile the registration to the post-retirement tier": after the
release converges, if the landblock is still loaded AND still Far tier,
re-assert the empty registration -- the identical OnLandblockLoaded(lb,
empty) a PublicationKind.Far activation makes. It is empty by construction:
DetachNearLayer retains only live server projections, which the adapter's
atlas-tier filter skips. A full retirement is unaffected (DetachLandblock
clears both _loaded and _tierByLandblock), and a throwing release still
retries because the re-assert is only reached after the adapter converged.
The alternative -- "|| (IsFarTier && IsLoaded)" at the gate -- was
rejected: it fixes one caller while leaving IsRenderReady meaning two
different things, which is precisely how this defect arose. After this
change the predicate reads "drawable at its current tier" for every caller,
with no knowledge of how the landblock got there.
WHY THE TESTS MISSED IT, fixed here too:
- Proof obligation P2 was discharged against RESIDENCY (the FarRadius+2
eviction threshold) rather than against IsRenderReady, the gate's actual
atom. The contract now carries the correction and the restated
obligation: no transition may REVOKE IsRenderReady from a landblock that
stays inside FarRadius.
- WorldRevealDerivedWindowIntegrationTests advertised itself as end-to-end
against the real GpuWorldState but constructed it with no spawn adapter,
so its IsRenderReady degenerated to IsLoaded via the "?? true". The
single most load-bearing predicate in the change was stubbed out by a
null in the test named after it -- the same shape as C5b's D3 and #276's
three settler tests. Every fixture in that file now owns a real
LandblockSpawnAdapter.
- The P1 test's comment described its subject as "a Near-shaped completion
the streaming window has since DEMOTED to Far". It is not; it is a fresh
PublishAsFar, the case that does hold. Corrected, since a future reader
would have taken it as demote coverage.
Four new regression tests, all driving the real GpuWorldState +
LandblockSpawnAdapter + LandblockPresentationPipeline through an actual
demote, and all sabotage-verified in both directions (fail with the
production change reverted, pass with it):
NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline
NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement
TieredWindow_StaysResidentAfterAnOuterRingDemote
OutdoorReveal_SurvivesAnOuterRingDemoteDuringTheHold
The budgeted variant exists because production composes
LandblockRetirementCoordinator.CreateBudgeted, whose MeshReferences stage
is a separate call site from the legacy pipeline's.
SECONDARY, same commit:
- R-1: ACDREAM_PROBE_REVEAL_RADIUS=0 was parser-accepted and
Runtime-rejected -- it yields far = 0 for an outdoor destination, which
fails invalid-readiness-shape on every acknowledgement, hanging the very
A/B route the probe exists to measure. Parser floor raised to 1, with a
7-case table test.
- R-2: the composite-warmup TRIGGER had silently moved onto the far
window's critical path. Pre-#280 the gate and the composite domain were
the same radius-1 square; #280 widened the gate without widening the
domain, so every composite upload serialised behind the last outer-ring
landblock for no readiness benefit. Warmup now starts once the NEAR
sub-window is published -- trigger scope == domain scope, as before. The
reveal gate is untouched: Evaluate still requires the full window AND
composite readiness.
- AP-150 filed: acdream's RetailWaitCueDelay = 5 s arming is NOT retail's
trigger, and #280's commit message got this wrong on both clauses. Retail
emits the notice unconditionally per tunnel rotation segment, in the else
arm of the segment-expiry test at 0x004D6FCD; segment duration is
RandDouble(0.6, 1.8) s, byte-decoded at 0x004D6FE6. The 5.0 constant at
VA 0x007991B0 is CellManager::CheckPrefetchStatus's prefetch RETRY
cadence and has nothing to do with the cue. acdream's own 0.6/1.8 segment
constants already match retail exactly; only the arming is wrong.
Adopting retail's unconditional emit is filed as #329 rather than folded
in here -- it is a user-visible presentation change and wants the user's
eyes.
- AP-151 filed: the gate is materially STRICTER than retail on the
mesh-build/GPU-upload axis. Retail's LScape::PreFetchCells blocks on DAT
RESIDENCY only -- no geometry construction, no upload; that work is lazy
at draw. acdream requires a DAT read, terrain mesh build, render-thread
upload, spatial commit, collision admission and spawn-adapter activation
per member of a 625-member window, metered at MaxCompletionsPerFrame.
Nothing bounds the hold. This is the OPPOSITE asymmetry from AP-149; both
are live at once, on different axes.
- AD-2's amendment stated the false Far-tier readiness assumption verbatim;
corrected, along with the same error in
claude-memory/reference_two_tier_streaming.md, which now carries an
explicit DO-NOT-RETRY on the special-case-the-predicate shape.
- AP-115 scope-noted (it covers the cue's presentation, not its arming).
- #326's SmartBox::set_mid_radius citation corrected: the entry is
0x00453180; 0x004531D0 is the mid-function re-arm branch.
Blast radius: GpuWorldState, LandblockSpawnAdapter,
WorldRevealReadinessBarrier and StreamingDiagnostics are all App-internal;
AcDream.Headless and AcDream.Runtime reference none of them outside
comments. Headless tests run green as part of the gate below, per C5b's
lesson about surveys that skip the no-window host.
Gates: Release build 0 errors, 18 pre-existing xUnit analyzer warnings.
Complete suite "dotnet test AcDream.slnx -c Release -m:1" with
ACDREAM_PAK_PATH set: 11,192 passed / 4 skipped / 0 failed, from a clean
rebuild (a prior session's deleted probe file had been compiled into a
stale test DLL). Baseline at
|
||
|
|
fafc0b65d9 |
fix(physics): adopt the settle's resolved cell across an indoor seam (#276 remainder)
SpawnPlacementSettler committed settle.Position but discarded settle.CellId, so a compressed first-gravity-frame settle that crossed a cell boundary left the body's cell at the placement cell until some later resolve corrected it. Now committed through the same guarded channel the per-tick resolve writeback uses (RuntimeOrdinaryPhysicsUpdater): resolved cell when the transition reports one, source cell otherwise, never a zeroed residency. Retail anchor: CPhysicsObj::SetPositionInternal(CTransition const*) 0x00515330 commits both sphere_path.curr_pos.objcell_id and its frame, including EnvCells. WHERE THE DEFECT ACTUALLY BIT — #276's own framing is half wrong, and the half it misses is the whole fix. PhysicsBody.Position's setter already mirrors the world delta into the landblock-local frame and lets LandDefs.AdjustToOutside recompute the 24 m cell index from it, so an outdoor->outdoor settle already landed the right cell and dropping settle.CellId cost nothing there. It cannot do that for an EnvCell: an EnvCell id is not derivable from a position, so the mirror deliberately PRESERVES it. settle.CellId is therefore the only carrier of a cell identity across an indoor seam. The live defect is the issue's parenthetical ("outdoor/EnvCell seam, stacked EnvCells"), not its main clause — and the change is consequently a no-op on the outdoor path that dominates production, corrective only at the seam. That finding is what made the test possible. The three existing settler tests build bodies with NO CellPosition and pass identically with or without this change — shipping against them would have repeated C5b finding D3, a test that passed with its own change reverted. The new test seeds an EnvCell id over plain outdoor terrain instead, so the stale-id preservation is the discriminator and no EnvCell geometry fixture is needed. Sabotage-verified: restoring `body.Position = settle.Position` fails exactly the new test (1 failed / 4) and leaves the other three green — confirming both that the new test discriminates and that the old ones never could. Core suite 4,263 passed / 1 skipped / 0 failed, +1 for the new test. Still open and unverified, deliberately not claimed closed: whether the remote spawn-seed caller (LiveEntityNetworkUpdateController) hands in a body that carries a CellPosition at all. CommitTransitionPosition early-returns on a zero cell, so this fix is an inert no-op there and #276's remote half may survive. The C3c local first-entry caller is confirmed — it passes activation.Body.CellPosition.ObjCellId. Scoping detail in docs/research/2026-08-06-276-remainder-scoping.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3aab05b0cc |
fix(streaming): derive the portal reveal window from the live streaming radii (#280)
The user watched far terrain visibly assemble after portal space exits.
The reveal gate was NOT missing a hold — Slice E's hold mechanism is
correct and already in place. The hold was measuring the wrong domain:
it opened at a hardcoded 3x3 landblock neighbourhood (~192 m) while the
visible world extends to the fog end (~2,189 m at the shipped High
preset, inside a 2,304 m Far window). An 11.4:1 ratio.
Retail's equivalent ratio is 1:1 BY CONSTRUCTION. `LScape` owns one
`mid_width x mid_width` array of `CLandBlock*` (`LScape::SetMidRadius`
@0x00504C00, `LScape::update_block` @0x005063A0), `mid_radius` is
assigned directly from the user's `Render.LandscapeDrawDistance`
preference (`SmartBox::SetRegion` @0x004531F0; values
`Render_LandscapeDrawDistance_Values` @0x007CA988 = {3,5,8,11,15,25},
default 8 — both byte-verified against the PDB-paired 2013 binary), and
that same square is simultaneously the prefetched set
(`LScape::PreFetchCells` @0x00505660), the drawn set (`block_draw_list`
over the same array), and the set the simulation blocks on
(`CellManager::blocking_for_cells`). There is no retail configuration in
which the client streams farther than it gates, because there is only
one number.
So the fix derives rather than duplicates. Four coupled parts, which is
why this is one commit and not four — D1 without D2 hangs the client and
D2 without D1 is dead code:
D1 `WorldRevealReadinessBarrier` takes a live `Func<StreamingRevealWindow>`
and stops being static: outdoor requires `FarRadius`, indoor still 0
(retail's `CEnvCell::PreFetchCells` @0x0052D1E0 arm). Read per
evaluation, never captured — the radii are runtime mutable through
Settings, and retail's answer to a mid-hold radius change is to reset,
re-radius, and re-arm the blocking prefetch at the NEW value
(`SmartBox::set_mid_radius` @0x00453180). `OutdoorNeighborhoodRadius`
is deleted; there is no constant left to drift.
D2 `StreamingController.IsRenderNeighborhoodResident` becomes tiered,
because acdream's loaded landscape is: inside `NearRadius`,
`IsNearTier && IsRenderReady`; out to `FarRadius`, `IsRenderReady` only.
Without this the fix cannot work at all — nothing outside the Near ring
is ever promoted, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever. Proof obligation P1 (a Far-tier
landblock genuinely satisfies `IsRenderReady`) is now a test driven
through the real `PublicationKind.Far` pipeline against a real
`LandblockSpawnAdapter`, not an inference.
D7 `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived
`indoor ? 0 : 1` and failed `invalid-readiness-shape` on any other
value, so changing the radius alone would have looked like "the fix
hangs the client". It is now a SHAPE invariant (`indoor => 0`,
`outdoor => >= 1`). Runtime does not own the graphical host's streaming
configuration and must not learn it; plumbing App radii into Runtime to
preserve the strict equality is exactly the assert-a-mechanism-that-does-
not-exist failure C5b was built to stop. Both non-graphical producers
keep emitting their centre-ring token and stay legal, annotated in place.
D6 `PhysicsEngine.IsNeighborhoodTerrainResident` rebuilt a full-map
`HashSet` on every call, every frame of every hold. At radius 1 that was
invisible; at radius 12 (625 ring members) it violates Slice I1's
0 B/resolve standard. Now an engine-owned scratch set, cleared in place;
measured at 0 bytes over 1,000 warmed radius-12 queries.
Also: the destination reservation opens at exactly the gate's radius and
reopens on the same generation when the radius changes mid-hold (retail
has one square for both, and no concept of prioritising an inner ring
differently). Composite warmup deliberately stays `NearRadius`-scoped —
the composite domain is entity-scoped and Far builds carry no entities,
so widening it would walk the outer window to warm nothing.
`ACDREAM_PROBE_REVEAL_RADIUS` is a measurement probe in a diagnostic
owner (CLAUDE.md rule 5) so the connected route can be run A/B on one
binary; it is NOT a user-facing prefetch knob, since a low setting would
reintroduce the decoupling this slice exists to close.
Register: AD-2 amended with the derived window, the two-tier split, and
the four new retail anchors. AP-149 FILED for the residual this does not
close — the outer ring accepts terrain-only publication where retail
requires LandBlockInfo and every building EnvCell, so a distant building
can still pop in at Far-ring distances. Do not let a later closeout
claim parity.
Docs: `ACDREAM_STREAM_RADIUS`'s CLAUDE.md description was wrong on every
clause (the default is unset, not 2; it forces `NearRadius`; it is
silently discarded by any Settings save) — corrected, since that is the
file every session reads. `reference_two_tier_streaming.md` corrected in
four ways, including "Far tier = terrain only": Far also publishes
terrain COLLISION, which is precisely what makes this fix viable.
#280's issue text had the right conclusion from a wrong premise (it
names a view-distance setting acdream does not have) — corrected, and
the missing Viewing Distance option filed separately as #326, with #327
(DDD progress readout) and #328 (hardcoded 5000 f far plane vs retail's
byte-verified 4000) filed alongside.
Expect LONGER holds and the "In Portal Space - Please Wait..." cue on
recalls MORE often. That is convergence toward retail, not away from it:
retail emits the byte-identical string for the whole duration of a
blocked prefetch and polls at 5 s intervals. The failure condition is
non-convergence, not duration.
Gates: Release build 0 errors. Complete suite 11,178 passed / 4 skipped
/ 0 failed, against a re-measured 11,142 / 4 / 0 baseline at
|
||
|
|
9ee9c1a1a6 |
fix(runtime): close the C5b re-review findings — Gate A narrowing filed, no-window payload gate, bisect hazard recorded
Both C5b re-reviews returned PASS on 02578441..ff100cf3. This lands the bookkeeping corrections they left, the one gate asymmetry both found independently, and one wrong retail fact neither of them caught. 1. AP-148 / #325 — Gate A's teleport test, wrong on primary source twice. The C5b contract stated retail's Gate A teleport term as "TELEPORT_TS equal" (and, in the trace block, as "must NOT be newer") and blessed acdream's `teleport == _timestamps[Teleport]` as retail-exact. Disassembly of the PDB-paired binary at SmartBox::HandleReceivedPosition 0x0045402B-0x00454054 says otherwise: the shortcut is taken iff the wire stamp is equal OR newer (wrap-safe) — `sbb eax,eax / neg eax` materialises the carry of the compare and the branch skips Gate A on CF, i.e. only when the wire stamp is strictly OLDER. It is CPhysicsObj::newer_event @0x00451B10's identical idiom with the operands swapped. Binary Ninja drops the flag test and renders it `if (-((eax_7 - eax_7)) == 0)`, always true — which is why two rounds of reading pseudo-C recorded it backwards. So acdream's ForcePosition disposition is a strict SUBSET of retail's Gate A set, and a local ForcePosition carrying a newer teleport stamp is misrouted into a full Apply: wire heading instead of preserved heading, unparent, possible placement frame, zeroed velocity, TELEPORT_TS advanced, and OfferTeleportDestination called for a packet retail never starts presentation for. PhysicsTimestampGate.cs is NOT changed. The predicate exists twice (also ValidAcceptedAuthority's PreviousTeleport == AcceptedTeleport), and the fix has to decide TELEPORT_TS's disposition on a Gate A path that has never seen a stale-but-equal pair. #325 records all of it and says explicitly that it is not a one-line comparison swap. C5b made this marginally better, not worse: clearParent was unconditionally true before C5b and is unchanged; installPlacementFrame moved toward retail's HasAnims gate. 2. Retail F2 / architecture L-A — the no-window route had no pre-merge payload validation. Root fix, not a documented asymmetry. The graphical route validates before the merge (OnPosition's payloadIsValid -> LiveEntityInboundAuthorityGate's !payloadIsValid return); despite its name CanAcceptPositionPayload is not projectile-scoped. The no-window route had no equivalent, and since D1 fed an unvalidated LandblockId into CommitWireCellRebucket — where 0 is the withdrawal shape, silently de-residencing the entity in the field every bot reads as CellId. RuntimeLiveEntitySessionController.OnPositionUpdated now applies the same rule at the same point, reusing RuntimeAuthoritativePositionRouteClassifier.IsValidCreateWirePosition plus the finite-velocity term — the exact pair TryApplyPosition already applies on its initial-residence branch. Chosen over documenting it because the fix is five lines and leaving it would have left two written claims falsified by the code. It is a behaviour change: headless now drops packets it merged. Against ACE the set is empty, and the graphical host has carried this gate since it was written; the argument is recorded in the contract's §15.2 rather than gated. Two test fixtures carried cell ids retail's own inbound_valid_cellid rejects (low words 0x41 and 0x51, above the 0x40 landcell ceiling). Their constants were corrected; their assertions were not. New test sabotage-verified in both directions: gate removed -> red at the withdrawal-shape assertion; gate moved to guard only the cell commit -> red at the pose assertion, which is what makes it a before-the-MERGE test rather than a before-the-commit test. 3. Register and doc corrections. - AD-64: "deliberately absent" was presented as the complete difference list and was not. Adds (a) the residence gate is weaker than the merge's own — both hosts' commits use TryGetCurrent while TryApplyPosition's FIFO branch uses TryGetTransaction, so the wire cell can commit ahead of the continuation that will replay it; (b) the two missile gates are two different expressions that agree today; (c) the payload gate, now present. Risk column records that (a) and (b) have no discriminating test on either side. - AP-147: amended for D1 — pre-D1 the no-window host published [Updated] alone and lost the Rebucketed, so a headless event log is now a real instance of the "consumer that snapshots a delta" the row warns about. - AD-60: "Matches retail exactly" scoped to the withhold, since the row's body documents two channels that do not. - CommitWireCellRebucket: notes the unreachable ThrowIfNull / EnsureNotDisposed precedence inversion. - TryCommitAcceptedWireCell: the discarded commit bool is explained rather than left bare — false means IsCurrent went stale, unreachable three statements after a synchronous TryGetActive. 4. Bisect hazard recorded in the C4 closeout handoff (the doc CLAUDE.md sends readers to before any C5 work) and in the contract's §15.3: commits 735f0a72..23aa62f2 contain a live headless defect — every remote's FullCellId frozen for the session — introduced by |
||
|
|
ff100cf33f |
fix(runtime): give the no-window host a post-merge canonical cell commit (D1, AD-60/AD-64, AP-146/#320)
C5b ( |
||
|
|
23aa62f292 |
fix(review): close the C5b architecture-review findings (D2/D3/D4, L1-L5, S1)
Follow-up to C5b ( |
||
|
|
735f0a72af |
fix(physics): classify before merge on every steady-state Position (C5b, #275, AP-131/AD-60)
The steady-state accepted-Position merge did two things retail never does,
on every single Position packet: it installed the wire placement frame and
unparented unconditionally, and it derived the record's FullCellId from
bare wire acceptance. Both are now correct, and they land together - a
half-flipped intermediate (classified flags with the wire stamp, or vice
versa) is exactly the mixed-residency state this campaign keeps paying for.
WHY the flags need no route. SmartBox::HandleReceivedPosition @0x00453FD0
decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A
@0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and ahead
of the HasAnims SetPlacementFrame gate @0x00454137. Neither gate reads the
near/far/teleport classification. So the two flags are a pure function of
(disposition, hasAnimations) and are computable inside the merge, pre-merge,
with no signature change, no route construction and no playerDistance - the
scoping's ~150-400-line route-plumbing estimate over-counted because it did
not see this. That truth table IS
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own
ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows; the classifier
stays the oracle and the equality is pinned by test, not by a shared path,
so each computation remains separately sabotage-verifiable.
WHY the cell is withheld. HandleReceivedPosition reads the wire objcell_id
into a LOCAL @0x00453FE3 and hands it only to BlipPlayer / TeleportPlayer /
MoveOrTeleport / ConstrainTo; it never assigns the object's cell. The
object's cell moves inside the placement family (SetPositionInternal
@0x00515BD0 to set_cell, enter_world) or per-frame transit, and nowhere
else. The continuation executor has encoded that rule since the executor
slice; this caller now matches it verbatim.
WHAT DELIBERATELY SURVIVES. Two steady-state wire-cell writers stay,
downstream of the merge and outside the classification window: the
OnPosition prologue rebucket (W2, into CommitRebucket), which is also the
local player's own cell-freshness path, and the post-routing wire-cell adopt
for non-placing arms (W3, AP-135). Gating W2 "for symmetry" would freeze the
player's canonical cell between teleports and #319's child-cell equality
would inherit the freeze. AD-60's rewrite names both so the retirement
cannot be misread as "wire acceptance never changes residency anywhere".
REGISTER. AP-131 RETIRED - the unconditional literals no longer exist; the
caller was corrected, not deleted, so the row's own "deleted at the
production cutover" framing is overtaken. AD-60's legacy half RETIRED and
the row REWRITTEN rather than deleted, naming W2/W3 (route 4b-3's D8
precedent: a silent whole-row deletion would hide surviving channels).
AP-130 amended - the merge consumes the same static HasAnimations proxy,
deliberately not escalated to a live animation-queue read. AP-146 and #320
amended - their "accepted inbound Position (RefreshSnapshot into
RuntimeEntityRecord.cs:234)" local-player cell writer is now the generic
tail's CommitRebucket, and a ForcePosition (which returns before that tail)
is placement-receipt-authoritative. #275 closed.
HEADLINE BEHAVIOURAL DELTA, stated once: a refused or contended local
ForcePosition now leaves FullCellId at the last committed cell where the
merge used to stamp the refused packet's wire cell. Retail cannot refuse
(AD-62) and its body keeps its last placed cell, so the new shape is the
retail-reachable one.
THREE CONSUMER SITES THE CONTRACT'S BLAST-RADIUS SURVEY MISSED, all
D2-caused, all found by the suite rather than by reading, all intended
semantics rather than regressions (recorded in the contract's new section
14):
(1) DatLiveEntityProjectionMaterializer's self-projection branch reads
FullCellId inside OnPosition's prologue recovery, ahead of W2. It now
correctly declines to project from an unplaced wire claim; production
installs the bucket at W2 in the same call (verified: no return between
the recovery call and W2 is conditioned on IsSpatiallyProjected or
FullCellId). Two hydration tests asserted the bucket at the recovery
boundary and now drive the production W2 step - the same shape as trap
T2, one layer up.
(2) ProjectileController.SyncPresentationFromResolvedBody writes
ParentCellId = record.FullCellId. On a refused missile placement that is
now the committed source cell. The MAJOR-1 invariant is unchanged and is
now asserted as the identity it always meant rather than as a wire-cell
constant.
(3) The merge's Rebucketed ternary does NOT become always-Updated as the
contract predicted, and is deliberately kept: the
Forget(restoreCancelledPark: true) above it can roll a wakeable
lost-cell park back, and RestoreParkWithdrawal restores canonical
residency. That is a real cell edge produced inside this method by a
placement owner.
TEST-COUNT RECONCILIATION. Baseline measured at this HEAD by stashing the
change: Runtime.Tests 1176, App.Tests 4135 (4132 passed / 3 skipped),
solution 11,106 passed / 4 skipped - matching the recorded figure at
|
||
|
|
6921a02744 |
refactor(physics): delete legacy PhysicsEngine.Resolve/ResolvePlacement/HasCellSurface (C5a, AP-1/AD-1)
Member-wise deletion of the three legacy resolver members named in docs/research/2026-08-05-c5a-contract.md: PhysicsEngine.Resolve, PhysicsEngine.HasCellSurface, and PhysicsEngine.ResolvePlacement. An exhaustive receiver census over src/ found zero production callers of any of the three — every production placement writer already reaches the canonical PhysicsEngine.SetPosition transaction exclusively through RuntimeSetPositionState (three call sites total). The deletion is purely member-wise: IsSpawnCellReady and AdjustPosition, which shared the same source region as the deleted members, are preserved byte-identical — every remaining production caller of either (including PhysicsCameraCollisionProbe, AdjustPosition's sole surviving production caller) is unaffected. Companion changes: - PlayerMovementController's 3-argument SetPosition test overload is renamed to SeedPlacementForTest (internal) and CommitPreparedPosition is deleted; 83 call sites across 19 test files were mechanically renamed to match. - Seven pinned test dispositions from the contract are executed: 3.1 (PhysicsEngineTests.cs: 11 legacy-resolver tests deleted, 6 ResolveWithTransition tests kept), 3.2/3.3/3.4 (re-point to canonical SetPosition, with TransitionScratchDifferentialTests.cs additionally gaining positive IsCommitted assertions after each bitwise comparison so the differential proves a placement actually committed, not just that two possibly-uncommitted results match), 3.5 (Runtime rename), and 3.6 (PlayerMovementPlacementTransactionTests.cs rewritten — its xmldoc now states plainly that the render-root publish moved to RuntimeSetPositionState.cs, but the sticky-release relocation claim was false and is retracted; this disposition's coverage loss is the sticky release path, not silently absorbed elsewhere). - Stale `PhysicsEngine.Resolve`/`Resolve` doc citations in CellTransit.cs, PlayerMovementController.cs, and HeadlessSessionWorldProjection.cs are corrected to name the surviving canonical entry points by symbol (SetPosition, AdjustSetPosition/AdjustPosition, ResolveWithTransition) rather than fragile line numbers. Retires AP-1 and AD-1 in docs/architecture/retail-divergence-register.md: both rows described production zero-delta placement routing remaining on the legacy resolver pending the Slice 4B2/4B route cutover; that resolver no longer exists, so the condition each row tracked is now structurally false rather than merely narrowed. AP-145 (routed through the prior commit) and this commit's AP-1/AD-1 together bring the section counts to 101 AP / 47 AD active rows. Builds on the AP-145 fix (previous commit) — this commit's staged tree was independently rebuilt and its four suites independently rerun on top of that commit before this commit was created, in addition to the combined rebuild/rerun below. Full-solution build: 0 errors (21 pre-existing warnings, all unrelated). Suite results (combined tree): Core 4270/4271 passed (1 skip; the single DatSoundCacheTests concurrent-decode-dedup failure is a known load-sensitive race, confirmed passing standalone and unrelated to this change), Runtime 1176/1176, Headless 86/86, App 4132/4135 (3 skips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f8e55ba5e4 |
fix(physics): route local-player shadow presentation through SyncPose (#318, AP-145)
RuntimePlacementPresentationSink.TryPublishPlace previously published the local player's collision-shadow pose with a direct LocalPlayerShadowState.Set call — a plain cache write that never touched PhysicsEngine.ShadowObjects. Because LocalPlayerShadowSynchronizer.SyncPose's own dedup check compares against that same cache, the direct write could pre-seed the cache with the destination pose and cause the next real SyncPose call to see "nothing changed" and skip its own ShadowObjects publish — leaving the real collision shadow at the pre-teleport position until an unrelated movement tick forced a real publish. Fix: TryPublishPlace now calls _localPlayerShadowSync.SyncPose(..., force: true), the same publisher ordinary per-tick movement uses, so Place always drives a real ShadowObjects write before the cache updates. TryPublishWithdrawal carried the exact mirror asymmetry (a bare LocalPlayerShadowState.Clear with no ShadowObjects.Suspend, leaving a live phantom shadow row at the park's source cell for the whole park window — the #184 shape) and is fixed in the same commit, same one-call shape: _localPlayerShadowSync.Suspend(entity). The sink no longer holds a direct LocalPlayerShadowState reference; both halves route exclusively through the one synchronizer, which owns the cache internally. The single LocalPlayerShadowSynchronizer instance is now constructed in LivePresentationComposition (before the sink) and threaded through LivePresentationResult to SessionPlayerComposition, which no longer builds its own — this guarantees the sink's Place/Withdraw edge and ordinary per-tick movement publish through the exact same publisher and cache rather than two independent instances that could drift out of sync with each other. TryPublishPlace's xmldoc now states the behavioural nuance directly: routing through SyncPose means Place inherits SyncPose's own admission guard (IsHidden, cellId == 0, not-current-visible-projection), which the old direct .Set() call never consulted. Under those conditions SyncPose now calls Suspend instead of publishing — correct and symmetric, but new behaviour worth flagging at the call site, not just in a test comment. RuntimePlacementShadowCompositionTests.cs (#318) proves four facts against the real ShadowObjects registry, not the cache: a bare Place publishes a real row at the destination cell with the source cell's row gone; a subsequent ordinary per-tick Sync is then a correct no-op; a Place for a registered non-local-player entity leaves its row at the source cell untouched and never touches the player's cache (route 7 P4 — the fix lives entirely inside the pre-existing player-only gate); and Withdraw suspends the real registry row, not just the cache, with the retained (suspendable) registration surviving for a later restore. All four were sabotage-verified in both directions. RuntimeForcePositionRenderCommitTests.cs (B2) drives a real end-to-end accepted ForcePosition through RuntimeEntityObjectLifetime.TryApplyPosition and RuntimeAcceptedPositionDriveController.TryExecuteAcceptedLocalPosition against a live HostFixture, asserting both the committed render position AND a cell change that deliberately crosses out of the spawn's outdoor grid cell, so the cell assertion is independently falsifiable rather than riding along with the position assertion. Retires AP-145 (this fix) in docs/architecture/retail-divergence-register.md. AP-1 and AD-1 are untouched by this commit — they retire separately in the deletion-sweep commit that follows. Evidence chain: docs/research/2026-08-05-c5a-contract.md (the governing C5a slice contract), docs/research/2026-08-05-c5a-architecture-review.md (round 1, FAIL — three MAJORs: vacuous route-7 P4 test, unfixed Withdraw-side mirror asymmetry, non-driving B2 test), docs/research/2026-08-05-c5a-architecture-review-round2.md (round 2, PASS with two MINORs — an unfalsifiable B2 cell assertion and the undocumented SyncPose guard nuance, both fixed here). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
392c1e22c1 |
fix(physics): bind a parented child to the parent's live incarnation (#319)
A player-parented child never received a canonical cell. Its FullCellId stayed 0 for its whole attached lifetime, so it could not follow the player across a boundary. Scope was wider than the local player: every REMOTE player's equipment too. ROOT CAUSE. EquippedChildRenderController hardcoded ParentInstanceSequence: 0 for a parented CreateObject. Correct for creatures and statics, which really are sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins (ACE Player_Networking.cs:37). The relation filed under (playerGuid, 0) while the record carried TotalLogins, so both route-7 write sites — D1's attach re-cell and D2's propagation lookup — keyed on an incarnation that never matched. TryCommitParent did not validate the sequence, so the attach succeeded and printed normally. Silent. A ROUTE 7 REGRESSION ( |
||
|
|
e0f96a55bf |
fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival. Portalling worked before this change and works after it — this is not a bug fix, EXCEPT that it found and fixed one dead-code production bug. THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the accepted destination at Place time, but TryBeginPortalReveal already consumes that slot at Aim time — so the arm was 100% dead code and every real portal Place refused with host-token-unavailable. Found only because we refused to accept 7 skipped tests instead of chasing the count to zero. RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING: SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with flags 0x1012, followed by PlayerPositionUpdated. BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed here (ConstrainTo @0x0045418A) and velocity is zeroed (set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook runs AFTER placement (@0x004538AE). THE THREE-ROUND DEFECT CHAIN, HONESTLY: - Round 1 released the player at the pre-teleport position while the anim stream marched on — the contract wrongly assumed Place re-fires (process rule 1's third occurrence this campaign). - Round 2's fix inferred commit from a global PendingCount, which three non-committing paths also clear — making the SAME bug complete cleanly and silently. Strictly worse than round 1: round 1 at least tripped portal-complete-before-materialized. - Round 3 latches the commit where it actually happens (ReconcileAndAcknowledgePortal), keyed on reveal generation and teleport sequence, via TryConsumePortalCommit. Two of the three required regression tests landed and are sabotage-verified on both hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted / HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted). The third (force-arm-takes-the-slot) was judged unnecessary on review: with the inference gone, PendingCount is only a "don't ask yet" guard at both gates, so a force operation occupying or vacating the slot no longer changes an input the commit decision reads — the case collapses into what the landed test already discriminates. THE B2/P3 RESOLUTION: both round-2 reviews were right about different branches of the same synchronous call. RuntimePlacementProjectionSubscription .OnPlacement acknowledges the FIFO head only when TryApply returns true; a Place whose portal authority went stale (transit ended/superseded while parked) used to return false, wedging every later entity's placement receipt behind it forever. Both sinks (RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink) now acknowledge-and-ignore a stale-authority Place instead of refusing it. The regression test (RuntimePlacementPresentationSinkTests .PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had been asserting the old, wrong `false` behaviour; it now asserts and sabotage-verifies the fix. Also lands: AP-144 (register discipline — the portal movement-event send reuses the stricter UsePositionFromServer gate where retail's SendMovementEvent is the looser autonomy_level != 0 test, diverging only at level 1, currently unreachable), AP-145 + issue #318 (the local-player collision-shadow presentation write bypasses its own publisher's ShadowObjects write via a direct cache .Set(), self-healing only once dedup diverges — filed, not fixed, pending a composition test), AD-42 deleted (its last citation retired by the canonical portal arm), AD-2 updated (the wait-cue's trigger predicate now covers a second cause), and two documentation corrections: the enter_world misattribution (both call sites are in SmartBox::HandleCreateObject, only one in the player branch — portal arrival is TeleportPlayer, not enter_world) and the stale "local player never reaches this path" comment on the generic-remote-render-pose write. Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing weakened. STILL OWED: the connected two-client gate, with ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines actually appear in the capture — and explicitly NOT scored as covering issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects directly). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cd3129e9d6 |
fix(physics): C4 route 7 — child cell propagation moves from a render tick into Runtime
Retail re-cells children when their parent crosses a cell, recursively, to unbounded depth. acdream did it from a RENDER tick, so headless parented children were cell-less forever and the canonical cell had two writers. This slice makes Runtime the sole authority and demotes App's tick to presentation-only. Contract: docs/research/2026-08-04-c4-route-7-contract.md; the research that unblocked it is docs/research/2026-08-04-retail-parent-cell-propagation.md ( |
||
|
|
36255af0f6 |
fix(physics): C4 route 5 — projectile authoritative placement (#276 partial)
Ports retail's missile Position handling into the canonical Runtime
placement owner instead of the deleted ApplyAuthoritativePosition
short-circuit. The Create/residence-window halves of the projectile
pipeline (RuntimeProjectile binding, TryBind's adopted-body branch,
the collision/shadow registration) were already canonical from prior
slices; this closes the remaining gap — how an ACCEPTED Position for
an in-flight missile is classified, placed, and presented.
Byte-decode (Step 1 hard gate, before any code was written):
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 disassembled from
the PDB-paired binary (Capstone, x86 32-bit thiscall). `ret 0x10`
establishes four stack args; [esp+0x7c] (arg5, the velocity pointer)
is never referenced in any of the three branches (teleport/near/far).
The retail reviewer independently reproduced this by searching the
whole function body for the `24 7c` mod/rm+disp8 encoding a
`[esp+0x7c]` read would require and found zero occurrences. This
retired a fabricated `?? Vector3.Zero` fallback in the deleted method
— retail's PositionPack::UnPack initializes an absent velocity to
zero and MoveOrTeleport never installs it; the projectile's Vector
channel (RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector)
remains the sole velocity authority for a missile. D-P5 in the
contract; the Runtime seam commits no velocity from the Position
packet at all.
The unbound-missile fix: RuntimeEntityObjectLifetime's
ClassifyRemoteAcceptedPosition now derives ProjectileAuthoritative
from a CONJUNCTIVE predicate — the Missile bit AND a bound
RuntimeProjectile whose Body is the canonical PhysicsBody — never the
bit alone. Retail places every non-player CPhysicsObj unconditionally
(there is no missile-specific placement gate in MoveOrTeleport or its
callers), so an unbindable or not-yet-bound missile taking the
ordinary remote tail is retail-faithful, not a fallback: the earlier
bit-only discriminator would have silently frozen it instead.
AP-141 records this as a deliberate, recorded divergence, not
fidelity. Retail mechanically WOULD arm a missile's ConstrainTo leash
on any nonzero MoveOrTeleport return: HandleReceivedPosition
@0x00453FD0's only kind test is player-vs-not, ConstrainTo
@0x00454272 has no kind test of its own, and CPhysicsObj::ConstrainTo
@0x00510520 creates a PositionManager on demand via
MakePositionManager @0x00510523 if one doesn't exist. acdream
deliberately does not construct that EntityPhysicsHost/
PositionManager/InterpolationManager chain for a ballistic body — the
route-5b split the C4 route 5 contract rejected — so a live missile
never shows an armed leash and never catches up via the near/
UnroutedCatchUp policy. This divergence is safe specifically because
ACE never sends UpdatePosition for a missile
(references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:
333-334, SendUpdatePosition() commented out inside the
PhysicsState.Missile branch at :265) — every half of this row is
deterministic-test-gated only, never exercised against a real server.
AP-141 also records the surviving ConstrainTo re-anchor divergence
under clause (b): for the adopted-body case (TryBind's shared-body
branch — an ordinary remote whose Missile bit is set by a later
State packet, so it still carries a live RemoteMotion), acdream now
ports retail's teleport-branch and far-branch StopInterpolating
action (Interp.Clear()), but never re-arms or re-anchors the
inherited ConstrainTo leash the way retail's HandleReceivedPosition
@0x00454254/@0x00454272 does on every nonzero return. The risk
column's earlier wording — that a stale leash "would drag the body
toward a stale anchor" — was wrong and is retracted in this same
commit: ConstraintManager.ConstraintPos is write-only in both retail
and the port (never read by AdjustOffset), and
ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an
already-composed per-tick offset while InContact — a leash brakes
motion the interp/sticky chain already produced, it cannot pull
anything toward the anchor. The real residual is one tick of un-reset
brake accumulator, contact-gated, and it cannot move an airborne
far-snapped missile at all (the clamp branch does not run while
airborne).
NO CONNECTED GATE EXISTS for this route, by design: ACE never sends a
missile UpdatePosition (see above), so retail's own server never
exercises this code path in play. Every proof obligation here is
test-gated only — Runtime and App-level fixtures constructing the
packet directly — never a live client/server capture.
Three review rounds closed 8 MAJOR findings before this landed:
round 1 (A1 App discarded the seam's status; A2/R1 silent swallow on
an unbound missile; A3/R2 the adopted-body teleport_hook never
wired; A4/A5 zero Runtime/App test coverage); round 2 (a
ParentCellId regression introduced by round 1's own R6 finding,
which the retail reviewer retracted the following round as factually
wrong — the fix here is the REVERT to record.FullCellId, not the
relocation round 1 shipped; B2 the far-branch StopInterpolating skip
never extended to the adopted-body case; residual App/Runtime store-
path coverage; a per-packet closure contradicting the file's own
#315 cached-delegate pattern). Round 3 closed on coverage alone (no
defect): the Advance() retry arm's projectile branch — added at
round 2, semantically reordered at round 2's B5 fix (skip prediction
invalidation on a re-parked Contention, since it writes nothing) —
had never been executed by any test; two new tests drive it directly
and are sabotage-verified against both the reordering and the
retry-arm's own SyncProjectilePresentation call site. The one
recorded defect this campaign produced (the ParentCellId regression)
was caused by complying with a review finding that its own author
later retracted — the standing lesson recorded for future rounds is
that review findings are evidence to re-verify against the code, not
commands to obey unconditionally.
Complete Release suite: 11,063 passed / 4 skipped / 0 failed
(baseline 11,036 at
|
||
|
|
edc911b042 |
refactor(physics): collapse OnPosition's dual player/NPC remote tail into one
C4 route 4b-3 collapse (docs/research/2026-08-04-onposition-collapse-contract.md). Behaviour-preserving: the ~640-line duplicated player-guid and NPC-guid copies of the remote routing tail in LiveEntityNetworkUpdateController.OnPosition become one guid-blind tail, reached by every remote guid through the single ApplyRemoteContactRouting/RunRemoteArmTail seam. Two guid-conditionals survive, both named and justified: - Row 8 (TS-44 sticky suppression, creature-only): retail's sticky is independent of this acdream-only steady-state gate; the register row already describes it as NPC-only and this collapse does not widen it. - The AirborneSnap arm's interp-clear + shadow-publish (rows 2a/2b, player-only preserve): unifying either way would be an unauthorized behaviour change. #316 (shadow publish) is a real, unmeasured pre-existing defect, deliberately preserved not fixed. The interp-clear's equivalence could not be proven for the steep-non-walkable-landing edge case (AdjustOffset's CONTACT-keyed gate vs. AP-139's WALKABLE-keyed per-tick clear) — preserved per contract stop condition 2 rather than shipped on an incomplete proof. Category-(c) resolutions (contract §2.1-2.5), each with its evidence: - Row 2a (interp clear): PRESERVED — AdjustOffset's `if (!inContact) return` proves inertness on flat landings, but not on the steep-contact edge case. - Row 2b (shadow publish / #316): PRESERVED — no design note ever sanctioned the player-guid skip; the file's own #184 Slice 2b comments contradict it. - Row 2c (EnsureRemoteMotionBindings): UNIFIED — the method is idempotent (`if (rm.Host is not null) return rm.Sink;`), so "always ensure" is safe. - Row 3 (wire-cell adopt ordering): UNIFIED — RebucketLiveEntity already commits the wire cell before either guid branch runs, so the deleted player-guid pre-write was a proven no-op. - Row 4 (LastServerPos/Time sample timing): UNIFIED — on a genuine first UP, InterpolationManager.Enqueue's already-close branch and the Snapped branch both converge on the same body pose/orientation for a zero-distance target. - Row 12 (wall-clock capture): UNIFIED — one shared `nowSec`, a microsecond-scale skew in acdream-only bookkeeping/diagnostics. Sabotage check (contract §5, performed and reverted, not committed): deleting the one remaining TryArmConstraintAfterOperation call failed 10/16 dual-guid matrix tests, spanning BOTH guid halves of every arming-dependent scenario (teleport, landing, near, far, sticky) — proof the matrix discriminates a defect regardless of which guid range exercises it, closing the class of bug that let 4b-3's A1/A2/R3 findings survive review when only one copy's tests were green. New tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs drives 8 scenarios x 2 guid ranges (0x50xxxxxx player, 0x8xxxxxxx creature) through the complete production OnPosition entry point. Doc comments on ApplyRemoteContactRouting, RunRemoteArmTail, ApplyWireAirborneLeftoverBookkeeping, TryAdoptWireCellAfterRouting, and the AirborneNoOperation throw guard updated to describe the collapsed one-path world (the "two callers stay one decision" claim was true before this commit and false after — fixed in the same commit that makes it false). One branch-routing source-text pin (LiveEntityNetworkBranchRoutingTests.cs) updated to follow the AP-140 CONTACT gate to its new address inside ApplyRemoteContactRouting. #316 stays OPEN, deliberately not fixed here — see its updated ISSUES.md entry. dotnet build AcDream.slnx -c Release: 0 errors. Verified independently bisectable at this exact commit: AcDream.App.Tests 4104/4107 (3 pre-existing skips), AcDream.Runtime.Tests 1125/1125. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
daef7c9835 |
fix(inventory): reset a split result's movement timestamps so recovery cannot throw (#314)
Found by C4 route 6's integration tests ( |
||
|
|
1b484937b6 |
test(physics): C4 route 6 — drops/split-recovery closure, zero production lines
Route 6 needs no production change and this commit contains none: C3c ( |
||
|
|
6dc7ba51ee |
feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.
Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.
D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.
Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.
Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.
A1 ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
creature — a regression this slice introduced while closing a
structurally identical hole. Now maps to NearInterpolate; switch made
total with a throwing default proven unreachable.
R1 D2's write-nothing shape existed on the player arm only; NPC packets
fell through and wrote the body. Retail makes no player/NPC distinction.
R2 report_collision_end(this,1) @0x00514F31 was bound to
ShadowObjects.Suspend, a port of a DIFFERENT retail function
(remove_shadows_from_cells) that teleport_hook never calls. Now routes
to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
ForceEnd in an admission-blocking transaction so a DoCollisionEnd
callback cannot recreate the contact table.
R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
(~1,000+ m/s) and planned a run cycle from it. Both the install and
RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.
BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.
Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.
Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.
Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).
Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.
Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.
Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.
Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).
STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|