Commit graph

429 commits

Author SHA1 Message Date
Erik
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>
2026-08-05 14:11:31 +02:00
Erik
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>
2026-08-05 03:57:37 +02:00
Erik
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 (ca96ea5e).

Retail: SetPositionInternal @0x00515330 branches on `this->cell == curr_cell`
@0x0051536d; the changed branch reaches change_cell @0x00513390, whose
delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over
children and write the FULL identity (add_object @0x00510ee2, objcell_id
@0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35).
change_cell itself has no child loop.

THE TRAP, recorded because it nearly shipped: the depth-1 loop
@0x0051539c-0x005153d8 is the SAME-CELL fast path (objcell_id and part-array
id only, deliberately not the cell pointer), NOT the propagation. An
implementer who finds it first concludes "depth-1, id-only" and strands every
equipped item at a landblock boundary — the #184 class. The clincher against
that reading: update_object @0x00515d10 early-returns on `parent != 0`
@0x00515d40, so a child never runs its own physics tick and parent
propagation is the ONLY mechanism maintaining its cell.

Route 7 performs NO placement (DoPickupEvent @0x00452240 = unset_parent +
leave_world; DoParentEvent @0x00452290 = set_parent + SetPlacementFrame), so
it arms ConstrainTo nowhere — the leash rule INVERTS relative to routes
2/4/5, and both reviewers confirmed nothing arms.

Propagation is an ITERATIVE WORKLIST, not recursion. The first implementation
recursed with a depth-64 cap; both reviews independently found the cap left a
truncated tail at a stale NON-ZERO cell — permanently unrecoverable, logged
only under a probe flag, and on the withdraw path exactly the #184 shape
AP-142 clause (a) exists to reject. Shipping a fresh #184 instance inside the
slice that fixes stranded children was not acceptable, so the cap was removed
rather than tuned. The worklist retires the cap, the constant, its register
clause, and the failure mode together. Termination: every record on the stack
is already at the target pair, so nothing can be pushed twice and a hostile
A->B->A cycle collapses without a visited set.

The child write deliberately bypasses the public RuntimeEntityDirectory
.SetFullCell and calls the record method directly. This is LOAD-BEARING:
the public method re-enters PropagateFullCellToChildren, which opens with
_propagationWorklist.Clear() — routing children through it mid-drain would
wipe the shared stack and silently drop every unprocessed sibling. Any future
side effect added to the public SetFullCell must be mirrored by hand at that
call site.

Deliberate divergence, recorded not disguised: retail's removal path leaves
children with a null cell pointer but a STALE nonzero objcell_id @0x005133c1.
acdream does not reproduce it, because FullCellId != 0 is the liveness
predicate at 45+ sites — faithful porting would mark dead children live.
AP-142 records this; clause (d) records that acdream cannot gate propagation
on HasPartArray the way enter_cell gates on part_array @0x00510ed8, because
the flag's only writers are graphical and headless never sets it — the reason
is Slice J LAYERING, not a semantic difference (retail's part_array is itself
a mesh-construction product, single assignment site makeAnimObject
@0x0050e930 -> CPartArray::CreateSetup @0x0050e93e).

D7 adopts retail's unset_parent-before-leave_world order @0x0045227f ->
@0x00452286, applied to BOTH pickup paths including the dormant executor
replay. Its inertness was verified by reverting it and finding all 12
propagation tests still green — reported honestly rather than papered over
with a manufactured test, and independently confirmed by both reviewers.

ClassifyLeaveWorld and its request/cause types are DELETED: retail has no
classification here, and method-per-cause IS the retail dispatch shape.
Wiring it would have forced a vacuous teleport-sequence predicate with the
#307 shape.

Two review rounds plus a coordinator-required third pass; 5 MAJORs. One was a
handoff failure worth recording: enter_cell's part_array guard was correctly
identified as load-bearing by the research, dropped by the contract when it
enumerated the writes, and inherited as an omission by the code — a right
finding that evaporated across two handoffs with nobody re-reading the source.
Another was a test that survived deleting the entire behaviour it claimed to
pin, because its assertion read a field written unconditionally one line
earlier.

NoProjection is structurally unreachable from TickChild (TryResolveExactAttachment
performs a strictly stronger form of the same guard one call earlier). Kept as
a fail-safe, unit-tested directly, and documented in two places rather than
wrapped in a fabricated end-to-end test.

Headless regression test — the direct gate for this defect, which FAILED
before this work because no code path existed:
RuntimeLiveEntitySessionControllerTests
.DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell.

Probe: ACDREAM_PROBE_CHILD_CELL=1 emits [child-cell] lines at all four write
sites (attach / headless-attach / propagate / withdraw / delete). TEMPORARY.

Complete Release suite MEASURED at 11,079 passed / 4 skipped / 0 failed
(baseline 11,063 at cff52c44, +16). An allocation flake appeared once under
load and was proven NOT this slice by reachability — RuntimeCollisionReportingState
contains zero SetFullCell and zero ParentAttachments references.

STILL OWED: the two-client connected gate (equip/unequip, carry across
landblock boundaries, pickup, loot, reconnect) with ACDREAM_PROBE_CHILD_CELL=1,
and a session counts only if [child-cell] cause=propagate lines appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:53:05 +02:00
Erik
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>
2026-08-04 16:00:10 +02:00
Erik
204d0ae047 fix(physics): remote bodies slide on steep faces instead of freezing (#32)
A remote observed in acdream landed on a sloped roof and froze; the server slid
on, the gap passed AP-87's 4 m threshold, and the body snapped — the visible
blip. Live probe capture, two adjacent ticks 63 ms apart:

  t=88420671  rsInContact=True rsOnWalkable=False rsIsOnGround=True
              bodyCpNz=0.6097 floorZ=0.6642 steep=True gravity=True
              vel=(2.146,2.264,-3.549)
  t=88420734  contact=True onWalkable=True   <- forced against the sweep
              gravity=False                   <- cleared
              velBeforeZero=(2.146,2.264,0.000)
              moved=0.0000                    <- and every tick after

The roof is 52.4 degrees against a 48.4 degree limit, so acdream's classifier
was CORRECT and was then overruled. Four independent links each froze the body
on their own: a per-tick force of Contact|OnWalkable, a per-tick velocity zero,
a Gravity clear at landing, and a landing edge testing IsOnGround
(= inContact || ...) instead of OnWalkable. The tick called
HandleAllCollisions alone — the tail of SetPositionInternal without its prefix.

Retail simulates remotes locally and derives these bits rather than asserting
them: CPhysics::UseTime @0x00509950 iterates the whole object table;
update_object @0x00515D10 gates only on parent/cell/FROZEN with no
is_player fork; SetPositionInternal @0x00515330 sets CONTACT from
contact_plane_valid @0x00515430 and ON_WALKABLE from contact_plane.N.z vs
floor_z @0x00515465-@0x0051548E before handle_all_collisions @0x005154FE;
set_on_walkable @0x00511310 fires HitGround @0x00511364 / LeaveGround
@0x00511346 edge-triggered with no ownership gate; calc_acceleration
@0x00510950 zeroes only when CONTACT && ON_WALKABLE && !Sledding @0x0051096B;
calc_friction @0x0050EE70 returns at its first line when ON_WALKABLE is clear.
acdream had copied retail's airborne no-op WITHOUT retail's local simulation.

The fix is mostly deletion: stop forging the transients, stop discarding the
authoritative velocity, stop clearing Gravity, and route the remote tick
through the same SetPositionInternal commit TickHidden and the local player
already use, with the landing edge derived from the sweep's own OnWalkable.
AP-87's threshold and conditions and InterpolationManager's node_fail_counter
snap-to-tail are deliberately untouched — this removes the CAUSE of the
divergence rather than weakening the backstop.

Cross-checked against ACE: its only creature-side VectorUpdate emitters are the
jump broadcast and spell projectiles, so integrating the wire velocity cannot
double-move a walking remote; and PhysicsGlobals.DefaultState already carries
Gravity, so deleting the manufactured State |= Gravity is safe.

Register: AP-81 narrowed (its GRAVITY half retired outright), AP-87 annotated,
AP-139 filed (the interpolation-queue clear on the landing edge), AP-140 filed
(the two routing gates select snap-vs-interpolate on walkability where retail
uses CONTACT — adjust_offset @0x00555D30 gates on transient_state & 1
@0x00555D52). AP-140's follow-up is deliberately shaped as "point the two gates
at Body.InContact", NOT "re-derive Airborne", which would perturb five writers
and collide with a pinned RemoteTeleportPlacementTests assertion.

Three gaps recorded in #32 rather than papered over: the new LeaveGround
dispatch is untested for chatter; a persistently !Ok transition can latch a
remote airborne; and — the visual-gate watch item — the deleted forge was a
blanket guarantee of Contact|OnWalkable, and contact_allows_move @0x00528dd0
silently refuses action animations without both, which is the literal root
cause of closed #270. Retail-correct on a steep face, a regression anywhere
else.

10 discriminating tests over a real PhysicsEngine landblock whose contact
normal Z is 0.61 against FloorZ 0.6642 — the live roof's exact relationship.
Suite 11,019 passed / 4 skipped / 0 failed. Includes the temporary
ACDREAM_PROBE_REMOTE_LANDING / ACDREAM_PROBE_REMOTE_SLIDE probe family that
produced the capture above; strip with the family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:21:16 +02:00
Erik
7f1c1f5aa6 feat(physics): C4 route 4b-2 — remote far snap through the canonical placement
Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for
remotes onto 4b-1's drive controller and deletes both legacy far blocks, both
duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The
4 m constant now exists exactly once. Teleport and cell-less stay legacy for
4b-3.

Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating
@0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8
regardless — the SetPositionError is discarded — so HandleReceivedPosition arms
ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity
decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch.
SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4.

Non-commit outcomes still advance the body, because retail's SetPositionInternal
@0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell
resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive
switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement
never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran
and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5).
Without this a refused far snap froze the remote with an emptied queue.

Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks
withdrew the entity (InWorld=false, clock suspended, residency removed) and were
never restorable, while Forget(restoreCancelledPark: true) runs for every
accepted Position on every entity. The restorable decision now lives inside
ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value
RestoreParkWithdrawal actually restores at — against every live quiescence
rather than one minimum-OperationId token. The three pre-snap fields are hoisted
into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents
passes restorableOnCancel: false explicitly; the plain unplaceable park is
provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so
a retained route-2 park cannot re-admit into a prefix that began quiescing
during the park.

CanAttemptDestination is retained as an OPTIMISATION only, with the two Core
predicates it cannot reproduce written down at the pre-flight, plus the two
properties that depend on it staying there.

Four fix rounds and eight Opus reviews. The slice was fully green at 10,990,
10,997 and 11,004 while containing real defects — a frozen remote pinned as
correct by its own test, a fallback that over-wrote on the exact retail paths
that decline to store, and a park guard incomplete on two independent axes.

Register: AP-137 (leftover classifications take AP-87's catch-up; states the
cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied
anyway, and the headless divergence), AP-138 (the refusable far placement),
AP-136 narrowed to match the relocation. #309's acceptance steps rewritten —
step 5 previously asserted a recovery the code does not perform — and gated on a
new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken.

Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline.
The 10,973 figure recorded earlier was wrong and is corrected here.

Connected gate outstanding: the two-client far-snap walk and #309.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 07:55:56 +02:00
Erik
eeec4fb42a diag(physics): remote landing-edge probe; record the two live jump defects
The user live-tested route 4a and reported two defects on player remotes: a
remote holds the falling animation after landing before finally landing, and a
remote jumping onto a house plants on the roof where retail slides off, then
blips to the slid-down position.

Neither is a route 4a regression. Do NOT revert 44830a0e — reverting would
restore the per-packet render slam 4a removed without touching either defect.

Bug B's root cause is identified and already covered by open issue #32, whose
text names both symptoms in one sentence. Both landing sites assert
TransientState |= Contact | OnWalkable unconditionally, where retail derives it
from the contact plane — CPhysicsObj::SetPositionInternal @0x00515330
(`if (contact_plane.N.z < floor_z) set_on_walkable(0) else set_on_walkable(1)`).
A steep roof is contact but NOT on_walkable; asserting both suppresses the slide
response, so the body sits until the server's positions walk 4 m away and
AP-87's threshold snaps it. That is the blip. Verified byte-identical pre-4a via
`git show 19d95094:`.

Bug B's *visible shape* IS 4a's: pre-4a every packet slammed the render entity
to the wire pose, so a stuck body flickered toward the true sliding position
5-10x per second — jitter rather than a clean hold.

Bug A stops at the goal's stop-condition rather than getting a speculative fix.
Three hypotheses with non-overlapping fixes; picking wrong means changing a
retail-ported gate on a guess. Retail's mechanism is already fully decoded, so
what is missing is OUR runtime state — no cdb trace against retail is needed.

Adds ACDREAM_PROBE_REMOTE_LANDING (PhysicsDiagnostics, read once at startup per
the diagnostic-owner rule, one bool check when off). It logs both landing sites
immediately before HitGround, and — the most diagnostic signal — emits a
separate line when a site is reached but the gravity gate is about to no-op,
which is hypothesis 1 (a wholesale Body.State write wiping the transient Gravity
bit mid-air, exactly AP-81's stated risk). Temporary instrumentation, marked for
stripping once the evidence is in.

Evidence recorded rather than new bugs filed: #32 gains the observation, the
root cause and the #173/AD-10 dependency caveat; AP-87 gains a live instance of
its stated risk; AD-10's stale file:line is corrected to RemoteMotionCombiner
with a note that its terrain-only normal cannot see a house roof at all.

Also files #308 — a SECOND flaky test, distinct from #302, which was twice
misattributed to it before being written down. #302 is a GC-allocation assertion
in App.Tests; #308 is a wall-clock deadline loop in Core.Net.Tests that fails
only under full-suite CPU contention (0 failures in 4 isolated runs). Conflating
them hides one, and an agent told to "ignore the known flake" would wave through
a real transport regression.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 01:10:03 +02:00
Erik
9b1e6fc637 fix(physics): #297 — keep the PWD bitfield live so PK status reaches the client
The user typed @pklite and then walked straight through other PKLite players.

Root cause: ClientObject.PublicWeenieBitfield was written exactly once, from the
0xF745 CreateObject parse, and never refreshed. ACE's only PK-change message is
PropertyInt.PlayerKillerStatus (134) over 0x02CE/0x02CD, which we parsed and
stored into Properties.Ints[134] but never translated back into the bitfield —
and ACE never re-sends a PublicWeenieDesc at all (EnqueueBroadcastUpdateObject
has zero live callers), so that property is the ONLY signal a client can learn
from. Both sides of the collision test read the frozen value, so
CollisionExemption's "4c. both PKLite -> collide" rule could never fire.

Retail's missing port: PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0
rewrites _bitfield in place — PK(4) -> (b & 0xfddfffff) | 0x20; PKLite(0x40) ->
(b & 0xffdfffdf) | 0x2000000; Free(0x20) -> (b & 0xfdffffdf) | 0x200000; else
b &= 0xfddfffdf. Mutually exclusive, verified byte-for-byte, with input values
confirmed against retail's own PKStatusEnum (acclient.h:6412-6427), not just
ACE's. Driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86.

The fix rewrites the value at its source rather than patching consumers. Two
review rounds were needed because the first pass missed that there are TWO
snapshot stores: InboundPhysicsStateController keeps its own private _snapshots
dictionary, and every untimestamped-field merge (ApplyAcceptedObjDesc and
friends) reads `old` from THAT store, not from RuntimeEntityRecord.Snapshot.
Refreshing only the active record left the target-side shadow flags correct
until the remote's next equip or unequip — ACE broadcasts an ObjDesc on every
one — at which point the appearance path rebuilt the registration from the
frozen spawn and dropped the bit permanently. The regression test demanded by
review is what surfaced that; it is verified discriminating (reverting gives
Actual: 8 instead of 33554440).

Five stores now hold this value, kept coherent from one source by two
ObjectUpdated subscribers plus the appearance-rebuild path. The two shadow-flag
writers are the same invalidation applied at the two edges that can invalidate
it, not competing authorities — review enumerated every drift path and closed
each. That coherence invariant is new as of this commit and is recorded as
register row AP-134, with AP-133 as the precedent for filing a row when the
danger is a future writer rather than current behaviour.

Also corrects TS-23's retirement narrative, which claimed every mover-flags call
site read the mover's "real" PK bits from 2026-07-30. The bits existed but their
source was frozen, so that only became true here; the site enumeration also
missed RuntimeSetPositionMoverPreparation, a seventh site that decodes the
snapshot directly.

Unblocks #298 (melee/missile admission needs the local player's own PKLite bit).
Follow-ups filed: #300 (Properties.Ints[134] vs bitfield mirror gap), #301 (same
defect class for radar blip colour and radar behaviour), #302 (a pre-existing
PortalProjection allocation-assertion flake, 1 in 6, found while verifying this
gate), #303 (LiveEntityPvpBitfieldSync is App-resident but Runtime-owned-state).

Gates: complete Release solution 10,895 passed / 4 skipped / 0 failed (baseline
10,887 including #299). Adversarial + retail-conformance review PASS after one
FAIL round. Every new test discrimination-verified by reverting the fix.
Connected acceptance NOT run — needs a live two-client PKLite session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:59:01 +02:00
Erik
88348f6791 fix(physics): #299 — port retail's mover-side IsImpenetrable exemption branch
CollisionExemption checked only the TARGET's IsImpenetrable, and the class doc
asserted "retail's pseudo-C only checks the target's IsImpenetrable(); acdream
follows retail" while blaming ACE for checking both. That was backwards: ACE was
retail-faithful and acdream was missing half the check.

Retail short-circuits on EITHER the mover's own state & IS_IMPENETRABLE (0x80)
OR the target's IsImpenetrable(); either alone exempts. Verified at the byte
level rather than from the decompiler's rendering — Binary Ninja shows the mover
test as `int16_t state_1 ... if (state_1 < 0)`, which reads like a 0x8000 test,
but decoding the PDB-paired binary at the mapped offset gives:

    8b 43 04   mov  eax,[ebx+4]     ; mover object_info.state
    f6 c4 01   test ah,1            ; 0x100  IsPlayer
    84 c0      test al,al           ; sign bit of AL = state & 0x80
    78 3d      js   ...             ; -> collide

`test al, al; js` is a byte-level sign test on AL, i.e. 0x80, not 0x8000.
Corroborated downstream in the same block (`test ah,8` = 0x800 IsPK,
`test ah,0x10` = 0x1000 IsPKLite) and by OBJECTINFO::init @0x0050cf30 setting
state |= 0x80 from the object's own IsImpenetrable().

Also corrected: ACCWeenieObject::IsImpenetrable @0x0058c8c0 returns
(_bitfield >> 0x15) & 1 — retail genuinely conflates BF_FREE_PKSTATUS with
"impenetrable", so acdream's FromPwdBitfield decode was already right.

Both retail arms set collide, so ordering between them is semantically free and
a misreading here could only ever produce spurious collisions, never a
walk-through.

Found while investigating #297; not symptom-causing on its own. No divergence
row: this retires a missing port rather than introducing a deviation, and
nothing in the register or the collision digest's DO-NOT-RETRY tables covers it.

Gates: complete Release solution 10,887 passed / 4 skipped / 0 failed
(baseline 10,867/4/0). Adversarial + retail-conformance review PASS on this
change specifically. Both new tests discrimination-verified by reverting the
branch and confirming failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:13:05 +02:00
Erik
89cf1e66d0 fix(physics): guard the world-frame agreement proven unreachable by measurement
Closes #283 (plan S3) - as UNREACHABLE, not by restructuring ownership.

acdream has two owners that convert a landblock-local network origin into the
streamed world frame: LiveWorldOriginState for presentation/streaming, and
RuntimePhysicsState.TryGetWorldFrameOffset for placement. They rebase on
different edges - Runtime the instant an accepted Position carries
TeleportAdvanced, App only once StreamingOriginRecenterCoordinator observes
old-window retirement completion, many frames later. A one-landblock
disagreement places an entity 192 m from the geometry around it: the same
failure family as the zero-offset bug 670f307c fixed, with a wrong origin
instead of a missing one.

The plan's first step was to prove or disprove reachability BEFORE moving
ownership, because a restructure on a hypothesis is churn. The probe added in
898ff18b answered it: a connected Release session recorded ZERO disagreements
across 11 completed reveals and six destination landblocks (0x0904, 0x1134,
0x3032, 0x8763, 0xA9B4, 0xF682) spanning roughly 45 km. A gap of even one
frame would have printed an offset in the tens of thousands of metres.

Cause of the safety: BeginOriginRecenter detaches EVERY resident landblock
before the new origin is adopted, so the two rebases are serialized and no
conversion can observe the gap. Ownership is therefore left exactly as it is.

What lands instead is the invariant that keeps it true.
LiveWorldOriginState.EnsureAgreesWithRuntimeFrame is checked at the
landblock->world conversion and is terminal on disagreement, converting a
silent 192 m-multiple misplacement into a loud failure with the offset in
metres and the landblock being projected. Six focused tests pin it, including
the cross-world portal case (0x09 -> 0xF6 = 45,504 m). Disagreement can no
longer reach the probe, so ACDREAM_PROBE_WORLD_FRAME now emits a verbose
per-conversion agreement trace - useful when a placement looks displaced for
some reason OTHER than a frame disagreement.

Complete Release solution: 10,844 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:33:46 +02:00
Erik
898ff18b26 diag(physics): probe whether Runtime's world frame and App's origin ever disagree
#283 step 1: prove or disprove reachability before restructuring ownership.

Runtime rebases its world frame the instant an accepted Position carries
TeleportAdvanced (RuntimePhysicsState.ObserveLocalWorldFrame). App's
LiveWorldOriginState rebases only once StreamingOriginRecenterCoordinator
.Advance observes IsOriginRecenterRetirementComplete - many frames later,
after the old window has fully retired. Between those two edges the owners can
disagree by the source-to-destination landblock delta, and anything converted
in the gap lands a multiple of 192 m from the geometry App is building. Same
failure family as the zero-offset bug 670f307c fixed, with a wrong origin
rather than a missing one.

Reasoning has already closed most of the window: the recenter detaches EVERY
resident landblock before adopting the new origin, so old-origin collision is
retired first. What remains is the narrow gap between Runtime's flip and App's
BeginOriginRecenter, while old-origin geometry is still resident. Whether that
is ever actually hit is an empirical question, and the campaign rule is that a
restructure needs evidence, not a hypothesis.

ACDREAM_PROBE_WORLD_FRAME=1 emits one [world-frame] line per DISAGREEMENT at
DatLiveEntityProjectionMaterializer's landblock->world conversion - the exact
App-side counterpart of Runtime's TryGetWorldFrameOffset, and the site that
already holds both owners, so no new dependency is introduced. Silence across
a portal run is the evidence that #283 is unreachable and can close as a
permanent invariant instead of an ownership move.

Measurement only: the probe never gates placement, and the flag lives in
PhysicsDiagnostics with the rest of the ACDREAM_PROBE_* family per the
diagnostic-owner rule rather than as a scattered env read.

Complete Release solution: 10,836 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:21:46 +02:00
Erik
71604331cf wip(physics): collision O(changed) delta-commit (O1-O3) - ON HOLD, feel-test failed
Publication-throughput rework per the D2 design (docs/research/
2026-08-02-collision-throughput-handoff/design-note.md): O1 per-prefix
installed-key ledgers replacing the seal's full-map scans; O2 per-
landblock delta commit (LandblockReplacementApplyCursor against the
active root) replacing whole-world TransferTo; O3 empty staging root,
commit-time reflood (CObjCell::init_objects 0x0052B420 ->
recalc_cross_cells 0x00515A30), journal/peer-rebase machinery deleted
(~1,900 lines net).

Automated gates green: Runtime 999, Core physics 2,135, App 4,039/3,
Headless 79, complete solution 10,812/0/4; lifecycle gate PASS
(connected-world-gate-20260802-193029). Soak 194423: publication-side
acceptance fully met (37 -> 4 failures, all convergence dims zero,
loadedLandblocks baseline-identical, waitCue 6/9 -> 1/9).

COMMITTED AS WIP ON USER DIRECTION - NOT ACCEPTED. The user feel-test
FAILED on this tree: monsters still pop into existence at close range,
monsters spawned mid-air far ahead, static placements visibly wrong,
plus 243x "Landblock already has a full retirement receipt"
InvalidOperationException catch-retry loop during origin recenter
(launch-feeltest-oclone.log). The 4 remaining soak failures
(pendingLandblockRetirements 131/122 at the Caul->Sawato stops) and the
implementer's "exposed pre-existing" classification are under
re-judgment against that loop. Dual reviews were dispatched and then
stopped mid-flight on user direction; NO review has passed this commit.
Full problem inventory + next-agent instructions:
docs/research/2026-08-02-collision-throughput-handoff/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 20:06:59 +02:00
Erik
529e0e9d88 feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):

- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
  shared RuntimeFirstEntryDriveController pumps both conductors from the
  placement-receipt flow; MaterializeProjection and RebucketLiveEntity
  are presentation-only while a residence is ACTIVE (ExecutorCompleted is
  the presentation-binding receipt); post-residence entities take the
  full legacy path including the prepare_to_enter_world clock edges.
  PlayerModeController attaches presentation to the Runtime-published
  controller; its legacy resolve/step-heights/host-construction path is
  deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
  exists; content-less sessions keep the pre-flip direct registration;
  SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
  prepared-collision read failure is a typed AwaitingCollisionSource
  retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
  controller mutation flows through the publication lifecycle.

Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
  Runtime ownership seam (post-logout ingest crash on the retired
  controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
  out of the seal (reentrant-commit RejectedAuthority), rearm generation
  identity corrected, PlayerModeAutoEntry requires the Runtime-published
  controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
  map-corner landblocks (grid row/col 0) fully legal through admission,
  park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
  SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
  retail first-gravity-frame touch (enter_world 0x00516170 carries no
  seed); the legacy unconditional force-seed is overwritten by a real
  floor-found contact; airborne spawns stay airborne; outbound contact
  bit verified end-to-end. Fixes the standing-cast 'You can't do that
  while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
  placement (HandleReceivedPosition 0x00453FD0 analog); register rows
  AD-61 (settle-timing compression now covering the local player) and
  AD-42 (repointed off the deleted resolve split) in this commit;
  residence-conversion owner API; wire-landblock guards; drive-pending
  ledger in IsConverged; route attach/detach latch; executor-drain drift
  model documented + source-pinned.

Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 18:10:33 +02:00
Erik
38fd4b8dc9 feat(runtime): own initial create residence transaction 2026-08-01 19:35:08 +02:00
Erik
0fbc7a1fb7 fix(runtime): preserve hidden setposition collision ownership 2026-08-01 18:22:45 +02:00
Erik
9b0f59bd1b feat(runtime): atomically replace collision generations 2026-08-01 17:33:34 +02:00
Erik
5785a07b3e feat(runtime): commit dormant SetPosition activation 2026-08-01 14:25:02 +02:00
Erik
99f867f053 feat(runtime): seal dormant SetPosition evaluations 2026-08-01 11:31:58 +02:00
Erik
237d1184d2 feat(runtime): own SetPosition collision reports 2026-08-01 00:15:11 +02:00
Erik
4c02ac4259 feat(runtime): own deferred set-position residence 2026-07-31 22:32:49 +02:00
Erik
e84a388e6f feat(physics): port canonical retail set-position core 2026-07-31 20:44:03 +02:00
Erik
6b28ff999c fix(physics): make collision activation starvation-free 2026-07-31 18:34:46 +02:00
Erik
d94145e6b8 fix(physics): seal collision generations before activation 2026-07-31 15:53:05 +02:00
Erik
be94bc9b06 fix(physics): activate collision generations atomically 2026-07-31 15:19:25 +02:00
Erik
3e0f3b6206 fix(physics): validate retail cell containment roots 2026-07-31 14:48:26 +02:00
Erik
7716c2ee89 fix(physics): restore retail cell availability semantics 2026-07-31 14:22:45 +02:00
Erik
75b6f6b6c9 fix(physics): restore retail path-6 collision response 2026-07-31 13:44:55 +02:00
Erik
acec33eca8 fix(physics): preserve retail step-down probe state 2026-07-31 13:24:25 +02:00
Erik
1fd5da67b4 fix(physics): restore retail step-down placement validation 2026-07-31 13:13:09 +02:00
Erik
4fbd93ecdb fix(physics): preserve retail edge-slide stop semantics 2026-07-31 13:03:46 +02:00
Erik
c559c48d80 fix(physics): restore retail edge-response ordering 2026-07-31 12:55:49 +02:00
Erik
4ca7230b36 fix(physics): hold retail cell across inner retries 2026-07-31 12:40:50 +02:00
Erik
67d1e9b331 fix(physics): preserve refreshed cell retry state 2026-07-31 12:31:34 +02:00
Erik
e5f855ac40 fix(physics): restore nested per-cell collision retries 2026-07-31 12:25:03 +02:00
Erik
c24bc571cf fix(physics): enforce retail step-down support radius (#273) 2026-07-31 12:10:03 +02:00
Erik
2dcb4f1d94 fix(physics): port retail stair edge backprobe 2026-07-31 09:26:28 +02:00
Erik
5a0f9868a6 fix(physics): port retail slope landing stop 2026-07-31 09:10:53 +02:00
Erik
bb1640f777 fix #270 closeout: strip investigation probes; close the issue
User-verified: casting fixed (exhaustion-edge gate) and monster attack
animations restored (spawn settle placement + lost-cell retry). Final
session evidence: 14/15 spawn settles grounded; Falling-refusal spam
collapsed 2,954 -> 15 transient pre-settle lines.

Strips the [UM-ACT]/[MT-FAIL]/[SPAWN-PLACE]/[remote-edge] probes, the
MotionInterpreter.DiagnosticGuid plumbing, and the two throwaway probe
tests (motion-table attack sweep, vitae color dump - both findings are
recorded in ISSUES/research). Complete Release suite: 10,030 passed /
5 skips / 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 07:26:57 +02:00
Erik
807fdb5f7f fix #270: retry spawn placement until first success (lost-cell recovery analog) + attributed probes
The creation-only spawn placement could no-op or fail during the login
flood (cell id not yet hydrated / streaming collision not resident) with
nothing retrying - monsters created in that window stayed
airborne-flagged forever and their action animations remained refused.
Retail's answer to 'object addressed before its cell exists' is the
CObjectMaint lost-cell list (GotoLostCell): park, re-place when the cell
is available. The UM dispatch path now retries SeedRemoteSpawnPlacement
while the body has never been successfully placed (no contact AND no
stored plane); one success ends the retries.

Probes: [SPAWN-PLACE] logs each placement outcome (guid/cell/ok/
contact/walkable); [UM-ACT]/[MT-FAIL] now carry the owning guid via
MotionInterpreter.DiagnosticGuid (probe-identity-attribution lesson)
plus the body's live contact/walkable flags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:57:38 +02:00
Erik
4da25a442b fix #270: run retail spawn placement at remote-body creation - standing monsters' attack animations restored
The [MT-FAIL] probe caught combat-stance monsters constantly failing to
dispatch 0x40000015 (Falling): their bodies were airborne-flagged while
standing. contact_allows_move (0x00528dd0) requires Contact+OnWalkable
and silently refuses every action animation for an airborne mover - a
spawned-standing monster's swings never played until it first moved.

Retail never has this state: CreateObject spawns run the placement
transition (CPhysicsObj::SetPosition -> SetPositionInternal 0x00515330),
which establishes CONTACT/ON_WALKABLE from the floor at spawn. Our
remote creation seeded a raw position with no placement.

SeedRemoteSpawnPlacement mirrors RemoteTeleportPlacement: engine
placement resolve (Setup-derived cylinder, TS-46) + the verbatim
CommitSetPositionTransition, wired at BOTH RemoteMotion creation sites
(UM-triggered creation - so a first-ever-UM attack animates in the same
packet - and ordinary first-UP creation). Unplaceable results leave the
body airborne exactly like a failed retail placement.

Also adds the [UM-ACT] (wire action items + stamp-gate verdict) and
[MT-FAIL] (refused animation dispatches) probes, riding
ACDREAM_DUMP_MOTION=1, which are what convicted the body state.

Complete Release suite: 10,032 passed / 5 skips / 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:28:57 +02:00
Erik
a46c8e65b2 fix #270: fire ReportExhaustion on the stamina-exhaustion edge only, not every stats tick
Retail calls CPhysicsObj::report_exhaustion from exactly one site -
CommandInterpreter::HandleExhaustion (0x006b3c70), a notification handler
for the stamina-exhaustion EVENT. Campaign P P1 wired it to every
movement-stats application instead (every stamina regen/drain tick), and
each call re-dispatches the current movement state through the animation
sink - truncating any in-flight action animation. The diagnostic session
log shows 490 spurious casting-stance re-queues in one short session:
'sometimes stuck in spell animations' was every stamina tick that
collided with a cast gesture's play window.

The re-apply now fires only when the exhausted state (stamina == 0)
transitions, matching retail's event semantics. Stats still reach
PlayerWeenie immediately via RuntimeMovementSkillProjection.ApplyTo.

Also adds the [remote-edge] probe (rides ACDREAM_DUMP_MOTION=1): one
line per remote HitGround/LeaveGround - each such edge drains the
mover's pending action animations (retail HandleEnterWorld), the
working theory for intermittently missing monster attack swings.

Complete Release suite: 10,026 passed / 5 skips / 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:11:20 +02:00
Erik
2d611b2b01 fix(physics): #265 landing-bounce family - retail check_contact seed + velocity-free landing commit
Retail jump landings BOUNCE: the floor touch records both a contact plane
(grounding) AND a collision normal (collided_with_environment), and
handle_all_collisions reflects the unmodified impact velocity off it at
5% elasticity (v += -(v.n)(elasticity+1).n, DEFAULT_ELASTICITY 0.05
@0x007c6a7c). Our transition already recorded both facts; the bounce was
suppressed by the AD-25 adaptation stack in the per-tick commit: a
Velocity.Z<=0 landing gate (needed because the resolver glued ascending
movers to the ground) plus a landing Velocity.Z=0 hand-zero whose stated
purpose was making the reflect a no-op. Downhill glided instead of
bouncing, flat-ground landings had no pop, and uphill jumps flapped
between grounded/airborne against the animation machine.

Three retail mechanisms replace the stack:
- check_contact (0x0050f5b0) seeding in ResolveWithTransition: a body in
  CONTACT seeds the transition's contact only while v.contactPlane.N <=
  0.0002; moving away seeds the last-known plane alone (get_object_info
  0x00511cc0). Ascending jumps therefore run contact-free (ballistic, no
  glue) - the gate's reason-for-being is gone. The plane requirement is
  strict: Contact-without-plane is unrepresentable in retail.
- SetPositionInternal-shaped commit (0x00515330, byte-read end-to-end,
  velocity-sign-FREE): contact purely from the transition's contact
  plane, HitGround on the airborne->walkable edge, HandleAllCollisions
  with unmodified impact velocity. Whole commit gated on Ok &&
  candidateMoved (retail pc:283657 skips SetPositionInternal entirely
  when the candidate did not move) - a standing body's contact state is
  never re-derived, which is what keeps rest bit-stable (AD-41 updated).
- Byte decodes: gate override state&0x800000=Sledding, zero branch
  state&0x20000=Inelastic, reflect strictly dot<0 - our port already had
  all three correct.

Settle: real landings (>=0.25 m/s) bounce and decay geometrically;
smaller impacts are consumed by retail's unconditional small-velocity
zero, so standing never micro-bounces. Re-baselines documented in place:
landing-survival pin measures decay post-settle; LiveCompare_Tick0/376
pin the new IsOnGround=false on zero-move ticks (captured true was the
retired seed echo; tick 376's captured body carries an 11.8 m/s grounded
velocity from the deleted get_state_velocity-overwrite era); de-overlap
fixture now carries the plane real grounded bodies always have. New
pins: LandingBounceSeedingTests (ascent no-seed, rest keeps contact,
strict plane, slope 5% reversal + tangential preservation, Sledding
override).

Investigation + implementation record:
docs/research/2026-07-30-landing-bounce-family.md. Complete Release
suite: 10,031 passed / 5 skips / 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:12:06 +02:00
Erik
06c76009f1 fix(physics): #265/#166 - stop zeroing grounded residual velocity, wire GroundNormal
Capture bisect (docs/research/2026-07-30-265-capture-bisect.md, mined
from artifacts/matrix-session2-resolve.jsonl records 3415-3434) traced
#265's lost roof slides / permanent landing freeze and #166's missing
downhill sled to a pre-existing (2026-07-20, ten days before Campaign P
- not a regression) mechanism in PlayerMovementController.cs's grounded
quantum block: it hand-zeroed Velocity.X/Y to exactly zero every tick
once OnWalkable whenever animation root motion drives the walk (the
production graphical local-player path), discarding any residual
horizontal momentum a fall left on the body before calc_friction
(AP-7/AD-55, already correctly ported) or PhysicsBody.
UpdatePhysicsInternal's Euler integrator ever got a chance to act on it.

Two changes:

1. PhysicsEngine.cs now syncs body.GroundNormal (the vector
   calc_friction dots velocity against, per retail
   CPhysicsObj::calc_friction 0x0050ee70's `contact_plane.Normal` read)
   from the committed ContactPlane.Normal at the same commit point that
   already publishes ContactPlane. GroundNormal had zero production
   writers before this and silently defaulted to Vector3.UnitZ forever
   - even surviving velocity would have been tested against a fake
   flat-ground normal on any real slope. Core-level, so player, remote,
   ordinary, and projectile movers all benefit uniformly.

2. PlayerMovementController.cs's grounded block no longer reconstructs
   Velocity at all for the animation-root-motion case (only the
   headless/test-controller get_state_velocity fallback still does,
   unchanged). Root motion continues to fully own commanded locomotion;
   this only stops destroying whatever Velocity already holds, letting
   it compose with root motion through the same ResolveWithTransition
   sweep exactly as retail's CPhysicsObj::UpdatePositionInternal
   composes both channels.

Symptom (a), the uphill-jump bounce, traces to a SEPARATE, byte-exact
(re-verified against acclient_2013_pseudo_c.txt:282647-282760),
already-closed retail mechanism (AD-25, PhysicsObjUpdate.
HandleAllCollisions's shouldReflect gate) - confirmed orthogonal to this
fix, not addressed here (see the research doc's as-fixed addendum §9.5).

Issue265SteepSlopeCaptureBisectTests.cs gains a composed harness
(ReplayRealRoofLandingComposed) mirroring PlayerMovementController.cs's
per-tick composition against Core types only, proving: the old model
reproduces the mined freeze exactly; the new model survives the landing
and slides continuously (the real captured geometry glides at constant
velocity per retail's own dot>=0.25 early-return - AP-7); a synthetic
dot<0.25 case shows genuine exponential decay via calc_friction; and a
synthetic uphill-bounce case proves the fix changes nothing about
HandleAllCollisions's reflection decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:28:28 +02:00
Erik
61e959169b fix #266: retail run-rate 800 branch is exact-equality sentinel, not a cap
Raw byte decode of MovementSystem::GetRunRate (0x006b0950, PDB-paired
binary): fild skill; fcom [800f]; fnstsw; test ah, 0x44; jp general —
the C2/C3 parity idiom whose 18/4 fall-through executes ONLY at
skill == 800 exactly. ACE read this as >= 800 ('max run speed?') and
Campaign P P1 inherited that misread when BN dropped the arithmetic,
flat-lining every maxed character at 4.5 (retail-true ~3.70, +21%) and
erasing the vitae differential (both 10200 and 15225 sat above 800).

The [stat-chain] live capture proved the enchant chain correct end to
end (vitae 0.67 -> eff run 10200 -> controller), isolating the formula.
General path byte-verified: (loadMod*(skill/(skill+200)*11)+4)/scaling/4.
InqMaxRunRate's skill=9999 probe gets ~3.6961, not 4.5.

Golden tests pin the 799/800/801 straddle and the maxed-skill vitae
differential; pseudocode doc §6 carries the decode plus a do-not-
reimport-ACE warning. Complete Release suite: 10,025 passed / 5 skips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:22:41 +02:00
Erik
a8a7d64b47 Revert "fix(physics): TS-4 retired — Path-6 steep-poly shortcut deleted"
This reverts commit 5e2be19b4e.
2026-07-30 17:02:22 +02:00
Erik
c0afcacbb2 fix(physics): movement-parity fixes - adjusted catch-up cap, autorun retail semantics, AP-30 retired
Ports CMotionInterp::get_adjusted_max_speed (0x00527D00, byte-decoded:
bare rate unless RunForward; forward_speed x 4.0 when running;
current_speed_factor proven a ctor-constant 1.0 at 0x00528C34) and swaps
all five interpolation catch-up call sites to it - retail's
fUseAdjustedSpeed_ static (.data 0x0081F418 = 1) makes this the live
branch, so standing/walking remotes now catch up at ~2x runRate instead
of 4x too fast (the #41/#165 presentation family). Autorun now hard-
forces Run for its duration and cancels on every fresh forward press
(CommandInterpreter::HandleNewForwardMovement 0x006b3d60 is literally
SetAutoRun(0,1)); the old test pin codified the divergence. AP-30
retired: retail Frame::is_equal genuinely uses the 0.0002 epsilon - the
row recorded a non-divergence. Three catch-up test pins re-baselined to
retail semantics with citations. Full Release suite 9,983/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:04:17 +02:00
Erik
252e806804 fix(physics): AD-55 retired — Sledding fast-sled constant is cos(10 deg)
Per docs/research/2026-07-30-ts4-116-oracle-plan.md Addendum (byte-proven
2026-07-30). Raw bytes of CPhysicsObj::calc_friction @ 0x0050ee70's
Sledding fast-sled branch (0x0050ef52-0x0050ef6a):

  d9 86 38 01 00 00   fld  dword [esi+0x138]    ; contact_plane.Normal.Z
  dd 05 28 6b 7c 00   fld  qword [0x007c6b28]   ; = 0.17453292519943295 (10 deg RADIANS)
  d9 ff               fcos                       ; st0 = cos(10 deg) = 0.984807753
  de d9               fcompp

confirm a genuine fcos opcode over a real 10-degrees-in-radians double
literal -- not a BN misdecompile of a raw float load. Retail truly
computes cos(10 deg) ~ 0.9848078 at runtime; ACE's 0.99999536f equals
cos(0.1745 DEGREES) -- the same radian literal evaluated in degree mode,
a proven ACE porting error carried into this port provisionally.

PhysicsBody.calc_friction's Sledding near-flat override now compares
GroundNormal.Z > 0.98480775f (cos 10 deg). Feel impact: retail's 0.2f
fast-sled friction override engages on any ground within 10 degrees of
flat; the old constant engaged only within ~0.175 degrees (never, in
practice).

Tests: two new boundary pins
(calc_friction_sledding_fast_override_engages_at_5_degrees_from_flat /
..._does_not_engage_at_15_degrees_from_flat) construct a tilted
GroundNormal with velocity purely orthogonal to the tilt plane (dot=0
exactly, isolating the Sledding-band friction value from the outer 0.25f
gate and the normal-removal step) and assert the exact pow(1-friction, dt)
decay on each side of the new 10-degree boundary.

Register: AD-55 retired (struck through, retirement note with the byte
decode).

Full AcDream.Core.Tests suite: 4063 passed / 1 skipped, no regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:56:26 +02:00
Erik
5e2be19b4e fix(physics): TS-4 retired — Path-6 steep-poly shortcut deleted
Per docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2 (the
decisive TS-4 confirming run). Retail's BSP layer has NO steepness test at
all (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793) — every airborne
hit, steep or shallow, falls through to the same unconditional
`SetCollide` + `Adjusted`. The L.4 slide-tangent shortcut (worldNormal.Z <
FloorZ -> project-and-Slid, with its own SetSlidingNormal write) is deleted
from both BSPQuery.cs's and FlatBspQuery.cs's Path 6 sphere0 branch.

Fixing FlatBspQuery.cs (the flat/indexed engine Slice I6/I7 made
production-authoritative) was necessary in this same commit: it carried an
exact structural duplicate of the shortcut, caught by
FlatBspQueryDifferentialTests.InstalledDat_LargeRandomizedSweep_HasZeroBitMismatch
(graph=Adjusted vs flat=Slid) once the graph side was fixed alone. Its
sphere1 branch is also brought in line with the #116 shape-1 fix landed
in db2889af (direct Collided + SetCollisionNormal instead of the deferred
SetCollide/shortcut treatment) — that parity gap existed since shape-1's
commit only touched BSPQuery.cs and the randomized differential sweep
didn't happen to exercise the narrow foot-clear/head-hit case until this
session's broader change surfaced it.

DECISIVE CONFIRMING RUN (Ts4SteepRoofWedgeCaptureTests, per the plan's own
required test-first order): added
FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor
— the same steep-roof drop as the existing pure-vertical fixture, but with
a small residual horizontal velocity (vx=-0.3 m/s), matching the realistic
live-play input (WASD, jump momentum) that validated the shortcut on
2026-04-30. With the shortcut removed, this variant converges cleanly to
the flat floor with zero freeze. The pure-vertical fixture, run
shortcut-removed, DOES still freeze — per the oracle plan's root-cause
trace (§1.2 Step E), this is the DEGENERATE case: AdjustOffset's crease
projection (Cross(ContactPlane.Normal, SlidingNormal)) is mathematically
orthogonal to a purely-Z gravity offset, crushing it to zero every tick
before TransitionalInsert can run again — present identically in the raw
decomp, ACE's port, and this port. Renamed and re-asserted as a PINNED
known-degenerate test
(FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity)
rather than treated as a bug. Filed as register row AD-56.

BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope_ReturnsSlid pinned the
OLD shortcut's Slid-no-Collide behavior directly; renamed to
...ReturnsAdjustedAndSetsCollide and corrected to the retail-faithful
Adjusted/Collide=true outcome.

Register: TS-4 row retired (struck through, retirement note); AD-56 filed
for the pure-vertical degenerate case; the retire-next shortlist's TS-4
entry removed and renumbered.

Full AcDream.Core.Tests suite: 4060 passed / 2 skipped (D4 stays Skip-tagged
in this commit; its own un-skip is a separate, dependent test-only commit
for #116 shape-2), no regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:51:44 +02:00
Erik
db2889afda fix(physics): #116 shape-1 — Path-6 head-sphere direct Collided return
Per docs/research/2026-07-30-ts4-116-oracle-plan.md §2.3-§2.4: retail's
airborne (not-yet-Contact) BSPTREE::find_collisions dispatch, when the
FOOT sphere is completely clear but the HEAD sphere hits or near-misses,
does not defer through SetCollide/Adjusted (nor the steep-poly
slide-tangent shortcut) — it records the head polygon's normal directly
and hard-stops: pc:323824-323834 (0x0053a793/0x0053a7a4), independently
cross-checked against ACE BSPTree.cs:221-230 (`SetCollisionNormal` +
`return TransitionState.Collided;`), an exact structural match confirming
this isn't a BN misdecompile. BSPQuery.cs's Path 6 `hasSphere1` branch now
does the same: `collisions.SetCollisionNormal(worldNormal1); return
TransitionState.Collided;`, replacing the old steep-shortcut-or-deferred-
SetCollide handling. This mechanically retires one of TS-4's two
`SetSlidingNormal` write sites (sphere1's) ahead of TS-4's own item.

Added two permanent diagnostics gated on the existing
PhysicsDiagnostics.ProbeIndoorBspEnabled flag (`[path-dispatch]` at
FindCollisionsCore entry, `[path5-diag]` inside Path 5) to make future
BSPQuery dispatch tracing cheaper.

HONEST RESULT of the plan's own confirming instrumentation (re-run of
DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals):
this fix does NOT change the tick-22760 outcome (harness still cn=(0,0,1)
vs live cn=(0,+1,0)). The new dispatch-entry probes show the tick-22760
mover is GROUNDED (Contact set), so it never reaches Path 6 at all — it
dispatches Path 5 -> StepSphereDown (Path 3, both DoStepDown half-steps
fail) -> EdgeSlideAfterStepDownFailed -> SpherePath.PrecipiceSlide, whose
find_crossed_edge-false fallback returns Collided with NO collision-normal
write. A fresh byte-level read of retail's SPHEREPATH::precipice_slide
(pc:274316-274326, 0x0050cc80) confirms this is byte-exact retail
behavior (`if (eax == 0) { walkable = 0; return 2; }`, no
set_collision_normal call) — not a bug. The real tick-22760 divergence is
further upstream, most likely this test's simplified door registration
(BuildEngineWithDoorFixture) not placing the door's BSP where live retail
actually intersected it, or a walkable-polygon state-capture gap — see
the research doc's Addendum 2 for the full trace and open candidates.

#116 shape-1 is therefore NARROWED, not closed: the Path-6 fix is a real,
independent retail-faithfulness improvement; the tick-22760 acceptance
criterion is not met by it and needs further harness/geometry work before
any further code change.

Full AcDream.Core.Tests suite: 4059 passed / 2 skipped, no regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:36:26 +02:00
Erik
7e1be3def0 fix(physics): TransitionalInsert returns the real exhausted-retry state
Per the TS-4/#116 oracle plan (docs/research/2026-07-30-ts4-116-oracle-plan.md
§1.4, §4 item 1): TransitionalInsert's retry loop hardcoded
`return TransitionState.Slid;` when the attempt budget exhausted, despite
the comment's own claim of returning "whatever the last iteration said."
ACE's equivalent (Transition.cs:933, `return transitState;`) and retail's
(pc:273363, 0x0050b949, `return edi;`) both reuse one state variable
across the composite per-attempt call and return whatever it holds.

acdream's per-phase dispatch (env/building/object/other-cells/neg-poly/
step-down) is split across several locals instead of ACE's single
composite call, so `transitState` is now re-synced from whichever
phase-local variable most recently caused a retry `continue`, and the
final return uses that real value instead of the hardcoded constant.

Blast radius is zero: ValidateTransition's "not OK" branch treats
Collided/Adjusted/Slid identically, and every caller of TransitionalInsert
either feeds the result straight into ValidateTransition/
ValidatePlacementTransition (both `== OK` vs. not) or checks `== OK`
directly. Full AcDream.Core.Tests suite: 4059 passed / 2 skipped, no
change in pass count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:36:05 +02:00