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>
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>
PhysicsBody.IsFullyConstrained now reflects real ConstraintManager state
(pushed every tick by the same per-tick pumps commit 2 wired), so
jump_is_allowed's already-ported gate (WeenieError 0x47) actually fires
while an object is rubber-banding hard against a server position
correction, closing the last piece of #167.
Housekeeping:
- Delete register row TS-35 (retired: the write side is no longer stubbed).
- Rewrite the stale doc comments on PhysicsBody.IsFullyConstrained,
ConstraintManager (class + IsFullyConstrained), PositionManager.ConstrainTo,
EntityPhysicsHost.PositionManager, and PlayerMovementController.PositionManager
that described the leash as permanently unarmed/stubbed.
- Close#167 in ISSUES.md citing the research doc and commits e0629145 /
7719d25b.
- Add an "as-ported" addendum to
docs/research/2026-07-30-constraint-leash-constants.md naming the actual
current seam owners (the doc's own open question flagged this as
implementer-verify-required post-J-slices).
- Update docs/plans/2026-07-29-physics-parity-campaign.md's P5 status and
CLAUDE.md's Campaign P summary to reflect #167's closure (items #153/#72
remain open in P5).
Verification: complete solution suite green - 9,978 tests, 5 skips, 0
failures across all 9 test projects (Core.Tests, Runtime.Tests, App.Tests,
Headless.Tests, Core.Net.Tests, Content.Tests, UI.Abstractions.Tests,
Bake.Tests, Cli.Tests).
Add ConstraintDistance (outdoor/indoor start=10/5, max=50/20), byte-decoded
from the matching retail binary (GetStartConstraintDistance 0x0050ebc0,
GetMaxConstraintDistance 0x0050ec10 - both x87-return functions BN elided).
Deliberately omits the vestigial player-vs-remote branch the disassembly
shows loads identical constants either way. Pins the ACE-inversion (ACE's
start mapping is outdoor 5/indoor 10, the opposite of the binary - the
binary wins). Adds a full-chain conformance test proving an armed,
over-strained leash actually blocks jump_is_allowed (0x47), not just the
bare stub-property regression already covered.
See docs/research/2026-07-30-constraint-leash-constants.md.
Campaign P Slice P4 Opus review verdict: FIX-FIRST. RestrictionObjPrevalenceInspectionTests
(commit 3b5e0992) found 103,766 of 729,888 installed EnvCells (1,293 landblocks -
the whole housing estate) carry a baked RestrictionObj. The AP-71 gate's
unconditional fail-closed default (CanMoveInto unmodeled) would have locked
every apartment/cottage/villa interior for every player, including its own
owner - a live regression, not the "inert in dev content" the original
register row assumed.
Ports ACCWeenieObject::CanMoveInto (0x0058da40, pc:407982-408056) and
RestrictionDB::IsAllowedIn (0x005ae8f0, pc:444493-444516) verbatim into
ObjectInfo.CheckEntryRestrictions:
- owner_iid == 0 or == mover's own guid -> admit (open/owner)
- no RestrictionDB (retail _db == 0, i.e. never authored or not yet
received) -> admit
- present RestrictionDB -> IsAllowedIn: open-to-public flag, OR mover
shares the house's allegiance monarch, OR mover's own guid is a
guest-table member
- unresolved restriction object -> fails CLOSED, exactly retail's own
fallback when GetObjectA can't resolve it (pc:704-716)
Wire feed (Core.Net):
- CreateObject.cs: HouseOwner (WeenieHeaderFlag 0x02000000), HouseRestrictions
(0x04000000), and Monarch (0x40) PWD-tail fields were parsed-and-skipped;
now captured. Also fixes the HouseRestrictions PHashTable header
misconception: the wire is ONE packed u32 (low 24 bits = entry count),
not a separate count(u16)+numBuckets(u16) pair - verified against
Chorizite's RestrictionDB.generated.cs. The old skip's byte-count
happened to match for realistic guest-list sizes, but a future
numBuckets value >255 would have corrupted the parse; now correct
regardless.
- GameEvents.cs/GameEventWiring.cs: new House_UpdateRestrictions (0x0248)
parser + wiring - retail's live guest-list refresh, whole-unit replace.
No-ops if the house object hasn't arrived via CreateObject yet.
- ClientObject/WeenieData/ClientObjectTable: HouseOwnerId, MonarchId,
Restrictions (new HouseRestrictionRecord) fields + merge-preserving
Ingest + targeted UpdateHouseRestrictions.
Physics wiring:
- PhysicsEngine gains an Objects (ClientObjectTable?) property, mirroring
the existing DataCache pattern - acdream's GetObjectA equivalent, used
ONLY by the entry-restriction gate.
- RuntimeEntityObjectLifetime wires Physics.Engine.Objects = Objects in
all three constructors, right alongside the table's own construction -
the same canonical table every other subsystem borrows from, never a
second one. This is the production fix: without it the gate still fails
closed on every restricted cell (unresolvable object), so the wiring is
load-bearing, not cosmetic.
Register: AP-129 narrowed (not retired) to the genuine remaining residual -
House_UpdateRestrictions' Sequence byte isn't used for staleness/reordering
rejection (low-probability, self-correcting), and outdoor CLandCell
restriction (a separate DAT structure) remains unported and unaffected by
this fix.
Tests: 15 new/updated in Ap71EntryRestrictionGateTests.cs (resolved-unowned
admits, owner admits, present-list-excluded blocks, present-list-included
admits, open-to-public admits, shared-allegiance-monarch admits, unresolved
blocks via null and via an empty table, plus two new end-to-end
PhysicsEngine.Objects-wired scenarios); 2 new CreateObject parser tests +
2 new GameEventWiring tests for the wire feed.
AcDream.Core.Tests: 4049 passed, 2 skipped, 0 failed.
AcDream.Core.Net.Tests: 761 passed, 0 skipped, 0 failed.
Complete solution suite: 9,961 total, 9,956 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P4 item 2. TerrainSurface.SampleWaterDepth now returns 0.1
(was collapsed to 0) for a partially-water cell's dry corner, matching
retail's ObjCell.get_water_depth / calc_water_depth (via ACE's unambiguous
C# port). ValidateWalkable's formula was already byte-for-byte verbatim
(ACE ObjectInfo.ValidateWalkable line 124); only the constant was collapsed.
The old collapse's justification ("0.1 destabilizes the feet-exactly-on-plane
contact-touch check because dist > EPSILON skips SetContactPlane that tick")
is structurally true of retail too - traced and confirmed this slice: in ALL
THREE implementations (retail, ACE, acdream) a skipped touch-reassertion is
NOT a fall, because Contact/OnWalkable are STICKY -
PhysicsEngine.ResolveWithTransition's onGround computation ORs the fresh
per-call ContactPlaneValid with the seeded, persistent
PhysicsBody.TransientState.OnWalkable bit (itself written back by the
caller's own sticky TransientState). PhysicsEngine.SampleTerrainWalkable's
isWater = waterDepth >= 0.45f threshold means the restore does not flip the
dry corner's water classification (0.1 still < 0.45) - only the sink-in
depth changes. Full Core.Tests suite green (4038/2 skips, up from 4026)
proves the sticky-bit argument held in practice.
WATER_CONTACT_TS (TransientStateFlags.WaterContact, declared but never
written) is now mirrored alongside CONTACT_TS/ON_WALKABLE_TS at every commit
point that writes them: PhysicsObjUpdate.ApplySetPositionContact (projectiles
+ remote teleport), PhysicsObjUpdate.CommitSetPositionTransition (remote
teleport placement), and PhysicsEngine's per-resolve body-state commit (local
player + remote dead-reckoning + ordinary movers via ResolveWithTransition -
the actual SetPositionInternal-equivalent path). No signature changes needed:
body.ContactPlaneIsWater is already fresh by the time each function runs.
CollisionShadowVerifier audit: no change needed. It diffs graph-vs-flat BSP
traversal outcomes (ObjectInfo/CollisionInfo/SpherePath fields already
including ContactPlaneIsWater); it never touches PhysicsBody.TransientState,
and the water-depth constant is computed identically upstream of both
traversal modes, so it cannot introduce a new graph/flat divergence.
Filed #264 for the three items research explicitly left open (none block
this port): no confirmed retail consumer of WATER_CONTACT_TS was found (an
xref scan wasn't attempted - bitmask reads aren't text-greppable); the
CLandCell ENTIRELY_WATER ethereal/swim exemption from terrain collision was
not cross-checked; jump-in-water/swim-animation effects were not
investigated (out of physics/collision scope).
Conformance: Ap10WaterSemanticsTests covers SampleWaterDepth golden values
(NotWater/EntirelyWater/PartiallyWater wet+dry corners), the isWater
threshold non-flip, WaterContact mirroring in both PhysicsObjUpdate
functions, and two settle-to-rest end-to-end PhysicsEngine.ResolveWithTransition
scenarios (water: sinks exactly waterDepth below the plane and sets
WaterContact; dry: rests exactly on the plane and clears any stale
WaterContact bit).
Register: retired AP-10 (92 active AP rows, down from 93).
AcDream.Core.Tests: 4038 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P4 item 1. Ports retail's CObjCell::check_entry_restrictions
(pc:308873-308912, 0x0052b6d0), called FIRST by CEnvCell::find_env_collisions
(pc:309576) before any BSP work, as ObjectInfo.CheckEntryRestrictions wired at
the top of the indoor branch of Transition.FindEnvCollisions.
Resolves the research doc's open question on restriction_obj's source: the
ACE cross-check (references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs:32,
66-67) plus an independent reflection probe of Chorizite.DatReaderWriter
2.1.7's own EnvCell.RestrictionObj field confirm it is a plain DAT-baked
uint32 gated by EnvCellFlags.HasRestrictionObj (0x8) - not a live wire
override. The BN pseudo-C's "count for an array alloc" read at the same
UnPack offset was the mis-attributed field-name collision
feedback_bn_decomp_field_names warned about.
CellPhysics.RestrictionObj is wired from envCell.RestrictionObj in BOTH the
dev/graph-fixture path (CacheCellStruct) and the production/prepared path
(CachePreparedCellStruct) - the latter already receives a live parsed
envCell for Position/EnvironmentId, so no bake-format change was needed.
The mover's own CanBypassMoveRestrictions (BF_ADMIN 0x100000 AND
BF_IMMUNE_CELL_RESTRICTIONS 0x400000, acclient.h:6452-6454) is decoded via
the same PWD-bitfield pipeline TS-23 established for PK/PKLite/Impenetrable
(EntityCollisionFlags -> ToMoverState -> ObjectInfoState moverFlags).
Remaining gap (filed as AP-129, replacing the retired AP-71 row): CanMoveInto
(house owner IID + guest/ban list) is unmodeled, so a genuinely restricted
cell fails CLOSED for everyone, not just intruders - matching retail's own
fallback when the restriction weenie can't be resolved (pc:704-716). Outdoor
CLandCell restriction (LandblockInfo.RestrictionTables, a separate DAT
structure) is explicitly out of scope for this gate.
Conformance: Ap71EntryRestrictionGateTests covers the pure gate logic
(NPC bypass, admin bypass, fail-closed, ordinary-cell no-op), the PWD-bitfield
two-bit AND decode, and three end-to-end Transition.FindEnvCollisions
scenarios proving zero behavior change for ordinary cells.
AcDream.Core.Tests: 4026 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P3 item 3. The wire parse (CreateObject's
PublicWeenieDesc._bitfield), the decode (EntityCollisionFlagsExt.
FromPwdBitfield), the per-GUID storage (ClientObjectTable.
PublicWeenieBitfield), and the exemption logic (CollisionExemption.
ShouldSkip) all already existed and were already correct -- every
mover-flags call site just fed a GUID-prefix IsPlayer heuristic instead
of the real per-entity PK/PKLite/Impenetrable state (retail
OBJECTINFO::init 0x0050cf30 state |= 0x80/0x800/0x1000).
Port:
- EntityCollisionFlagsExt.ToMoverState translates the decoded PWD
bit-space into the ObjectInfoState bit-space FindObjCollisions
actually reads -- two different numberings that must not be
confused. Deliberately does not translate IsPlayer (every call site
already derives that correctly from its own GUID heuristic per
#184 Slice 2b).
- EntityCollisionFlagsExt.ResolveMoverPvpState is the one shared
ClientObjectTable-backed lookup (guid -> ObjectInfoState), replacing
what would otherwise have been three separate inline copies across
GameWindow/LivePresentationComposition/RemoteTeleportController.
- Threaded as a new optional moverPvpState parameter through
RuntimeRemotePhysicsUpdater.Tick/TickHidden and
RuntimeOrdinaryPhysicsUpdater.TryBegin (default None preserves every
pre-P3 caller unchanged), and as PlayerMovementController.OwnPvpFlags
for the local player's own two resolve call sites.
- TS-23 section 12b: PlayerWeenie.JumpStaminaCost's pk parameter now
reads the real PlayerKillerStatus(0x86)/LastPkAttackTimestamp(0x91)
pair against retail's 20-second recency window
(pkStatus in {4, 0x40} && (timestamp + 20.0) >= now), replacing the
P1 hardcoded false. RuntimeMovementSkillState/Snapshot and
LiveSessionEventRouter.RecomputePvpStatus push both the PWD bitfield
and the PlayerKillerStatus pair reactively, riding the SAME
ClientObject event triggers RecomputeBurden already uses.
- A conformance test caught a genuine precision bug in the first
PK-timer clock choice: DateTimeOffset.UtcNow's Unix-epoch seconds
(~1.7 billion) loses ~128 seconds of precision in a 32-bit float,
silently swallowing the entire 20-second window. Switched to
Environment.TickCount64 (small, monotonic magnitude) -- also the more
retail-plausible basis, since LastPkAttackTimestamp is itself a wire
PropertyFloat and retail's Timer::cur_time is almost certainly a
process/session-relative counter for the same precision reason, not
an absolute epoch.
Non-PK invariant (the acceptance criterion): an entity with no
ClientObjectTable row, or a row whose PublicWeenieBitfield is null or
0, resolves to ObjectInfoState.None -- a no-op OR into moverFlags,
bit-identical to every pre-P3 caller's hardcoded value. A dedicated
test drives two real ClientObjectTable rows through
CollisionExemption.ShouldSkip and confirms PK-vs-PK collides while
PK-vs-non-PK and non-PK-vs-non-PK both stay exempt (walk through).
Register: TS-23 retired (both the collision-flags and PK-timer halves);
the stale "M2 combat must land TS-23" phase-gate note removed.
dotnet build + dotnet test (Core.Tests 4008/2 skip, Runtime.Tests
425/0, App.Tests 3968/3 skip, complete solution build) all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P3 item 1. Retail CPhysicsObj::transition (0x00512dc0)
seeds the collision sweep from CPartArray::GetSphere (the Setup's own
<=2-sphere list, each origin+radius scaled by m_scale) via
SPHEREPATH::init_sphere (0x0050c670) -- not from a symmetric two-scalar
(radius, height) capsule reconstruction. The human Setup 0x02000001's
authored spheres are (0,0,0.475) r=.48 and (0,0,1.350) r=.48; the old
reconstruction from (0.48, 1.835) produced (0,0,0.48) + (0,0,1.355), a
5 mm head-center offset the TS-46 register row documented as a residual.
Port:
- SpherePath.InitPath gains a sphere-list overload (ImmutableArray<
FlatCollisionSphere>, scale) sharing a new InitPathCore with the
existing (radius, height) overload, which is now the degenerate
2-scalar case of the same code -- byte-for-byte unchanged, so every
captured-fixture replay (CellarUpTrajectoryReplayTests,
DoorBugTrajectoryReplayTests, CellarLipWedgeTests) keeps passing
unmodified.
- PhysicsEngine.ResolveWithTransition gains optional sphereList/
sphereScale parameters; empty/default preserves the legacy scalar
path for every pre-existing caller.
- LiveEntityMotionRuntimeController.GetSetupMoverShape is a new sibling
of GetSetupCylinder (left untouched) that resolves the Setup's own
sphere list plus Setup-derived step-up/step-down
(CPartArray::GetStepUpHeight/GetStepDownHeight, 0x005180d0/0x005180f0,
x ObjScale, 0.4 m fallback matching the pre-existing literal).
- Threaded through PlayerMovementController (both resolve call sites,
new SphereList property set by PlayerModeController.ApplyStepHeights
and the Headless world projection), RuntimeRemotePhysicsUpdater
(Tick + TickHidden), and RuntimeOrdinaryPhysicsUpdater.TryBegin.
Remote/ordinary step heights are now Setup-derived instead of a
hardcoded 0.4f literal. Projectile and camera-probe sweeps are
untouched (already single-sphere-exact).
- PlayerModeController.ApplyStepHeights also now applies the x ObjScale
multiply to the player's own step heights (previously only the
remote/ordinary paths did), closing an adjacent gap the P3 research
flagged.
Ts46SphereListConformanceTests proves the sphere-list overload sees the
exact dat spheres (not the reconstruction), that the scalar overload is
unchanged, and that ResolveWithTransition's sphereList parameter
actually drives the sweep (a decoy-scalar control pair using a
head-height obstacle sphere).
Register: TS-46 retired (both residuals it named are closed); header
count corrected to 40 active TS rows.
dotnet build + dotnet test (Core.Tests 3991/2 skip, Runtime.Tests
425/0, App.Tests 3968/3 skip, complete solution build) all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All seven review lenses pass. CanJump's polarity is upgraded from
plausibility to proof: raw bytes of 0x00591b50 show fld load / fcomp
[0x007c5e24 = 2.0f] / test ah,5 / jp -> return 0, i.e. return 1 iff
load < 2.0 with unordered refusing - exactly the shipped code, NaN edge
included. UN-8 deleted. CACQualities::JumpStaminaCost's pk flag decoded
for P3: PlayerKillerStatus in {4,0x40} AND PropertyFloat 0x91 + 20 s >=
now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports the retail CACQualities/EncumbranceSystem/MovementSystem chain
(named-retail decomp pc 256393/412901-414050/416169-416320/695958+) so
PlayerWeenie's run rate, jump height, jump permission, and jump stamina
cost are real functions of burden, current stamina, and vitae/skill
enchantments instead of stubs.
Core:
- New EncumbranceSystem.cs (delegates to the already-verified
BurdenMath formulas — one source of truth for the burden HUD and
movement physics) and MovementSystem.cs (GetRunRate/GetJumpHeight/
JumpStaminaCost/GetJumpPower, decomp-cited; ACE cross-referenced
where BN dropped the general-case arithmetic entirely).
- PlayerWeenie rewritten as the CACQualities-shaped composition:
CanJump gates on burden (<2.0 load, UN-8 — x87 polarity resolved by
plausibility, Ghidra MCP unavailable this slice), JumpStaminaCost
returns the real ceil((load+0.5)*power*8+2) cost and always affords
it (matches decomp — retail's own function never refuses; "weak"
jump comes entirely from the stamina==0 skill-zeroing gate inside
InqRunRate/InqJumpVelocity, not a hard refusal), SetStamina wires a
null="unknown, don't gate" sentinel preserving every pre-P1 test.
- EnchantmentMath.GetMod gained an optional StatModType flag filter
(GetSkillMod convenience wrapper) so the SAME vitae/family-stacking
machinery already used for vital-max buffs now also answers "what's
the vitae+skill-enchantment-adjusted Run/Jump skill" — reusing the
M3 active-enchantment state, not a new engine.
Runtime:
- RuntimeCharacterState now stores the pre-EnchantSkill base run/jump
skill and recomputes the adjusted value (vitae first, then matching
Skill-flagged buffs, floor 0.5, truncate) on every base push AND on
every Spellbook.EnchantmentsChanged notification — a vitae change
alone moves the produced rate without a fresh PlayerDescription.
- RuntimeMovementSkillState extended with Burden/CurrentStamina
(RuntimeMovementSkillProjection.ApplyTo pushes both through the
existing seam); LiveSessionEventRouter recomputes burden from the
same Strength+aug-property+EncumbranceVal inputs the burden HUD
already assembles (reacting to the same ClientObjectTable events)
and pushes current stamina from LocalPlayerState vital updates.
- Wires the previously dead-lettered ReportExhaustion() R3-W4 seam:
LiveSessionRuntimeFactory's OnMovementStatsUpdated callback re-
applies the current snapshot to the live controller and forces an
immediate movement re-evaluation on any skill/burden/stamina change.
Register: retires TS-5 (CanJump/JumpStaminaCost stubs) and AP-25 (no
vitae in pushed skill). Adds AP-127 (two minor unmodeled retail bonus
properties + the stamina-buff-adjusts-local-copy nuance, deliberately
out of the bounded "run/jump query path only" scope) and UN-8 (the
CanJump x87 polarity call, flagged for a future Ghidra MCP
confirmation pass). Extends TS-23 (PlayerKillerStatus not parsed) to
cover JumpStaminaCost's new pk parameter, hardcoded false pending P3.
Full pseudocode + retail citations + the vitae/skill-level finding in
docs/research/2026-07-30-stat-coupled-movement-pseudocode.md.
Release suite: Core.Tests 3977/2 skips, Runtime.Tests 425/0 skips,
App.Tests 3968/3 skips — all green. (One pre-existing, unrelated Debug-
only flake in LandblockBuildOriginTests reproduces on the pre-P1
baseline and passes in Release; not touched here.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P2 step 3 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md
§1, §6 Step 5). The named retail decomp (CPhysicsObj::calc_friction,
pseudo-C:276694-276822, 0050ee70) independently re-confirms the 0.25f
threshold (derived twice, once per BN-rendered branch); the in-code claim
that "the decompile uses 0.0" traced to the older, unnamed FUN_0050f940
Ghidra chunk at a different address -- per CLAUDE.md the named decomp wins.
calc_friction now reads angle = dot(Velocity, GroundNormal); if (angle >=
0.25f) return; then unconditionally removes the normal-aligned velocity
component, then applies the existing (already-present but previously
unreachable) PhysicsState.Sledding-gated friction overrides. The BN-rendered
"two duplicated branches" around the state check is adopted as a single
linear function matching ACE's PhysicsObj.calc_friction shape -- the branch
split is most likely a BN decompiler artifact around one `if (state &
SLEDDING_PS)` block (ACE-derived, Ghidra-verify; low implementation risk
either way since ACE's reading is adopted regardless).
Why this doesn't repeat the reverted 2026-04-30 L.3c regression (naive 0.0
-> 0.25f bump dropped forward locomotion 3 -> 0.16 m/s): that test predates
the 2026-07-17 R6 "local player animation-owned grounded movement" landing.
PlayerMovementController (Runtime/Gameplay, out of this slice's scope) zeroes
Velocity.X/Y to exactly zero every tick before calc_friction runs whenever
animation root motion drives the walk, so friction has nothing horizontal
left to hammer on the production graphical local-player path. Pinned at the
PhysicsBody level (the only file this slice may touch) by
GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests. The
headless/get_state_velocity path and remote/NPC movers still feed real
velocity into this function and remain the ones to watch if a similar
regression resurfaces there -- flagged in the retired AP-7 row for future
sessions working in Runtime/Gameplay.
Left an open, explicitly-flagged discrepancy: the raw decomp's Sledding
slope-flatness test computes cos(10 deg) (~0.984808) while ACE's port (and
acdream's prior dead code) compares GroundNormal.Z > 0.99999536f (~0.175 deg
from flat) -- physically different tests, neither confirmed this pass
(Ghidra MCP down). Kept 0.99999536f provisionally (least churn) and filed
AD-55 for just that constant rather than silently picking one.
Register: AP-7 retired with a corrected citation; AD-55 filed for the
cos(10 deg) question. Core.Tests: 3916 passed, 2 skipped (both pre-existing
and unrelated), 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P2 step 1 (docs/research/2026-07-30-response-layer-edge-family-pseudocode.md
§2, §6 Step 1/2). The TS-1 register row (retail-divergence-register.md:238)
described work that is already done: SpherePath.PrecipiceSlide,
Transition.CliffSlide, and Transition.EdgeSlideAfterStepDownFailed are real,
tested ports of retail's edge_slide -> precipice_slide/cliff_slide chain
(pc:274316, pc:272397, pc:273001-273090). Its cited :1254 line was stale
stepping-loop code the file moved past.
The one real remaining gap (the back-probe fallback skipping retail's
walkable_check_pos/localspace_sphere recache before its second
precipice_slide call, pc:274318-274326 / 0050b4e0-0050b507) needed no
production code change: a fresh read of SPHEREPATH::get_walkable_pos
(0050a8f0), cache_localspace_sphere (0050c9d0), and set_walkable_check_pos
(00509ce0) shows that machinery exists to re-project a sphere across
retail's PER-CELL local coordinate frames. acdream's SpherePath.WalkableVertices
and GlobalSphere are populated in UNIFIED WORLD SPACE at assignment time
(SetWalkable/SetWalkableTransformed, SetCheckPos/RestoreCheckPos), so both
operands BSPQuery.FindCrossedEdge compares are already commensurable --
retail's recache is a no-op correction under this architecture, and
FindCrossedEdge never reads a sphere radius, so retail's walkable_scale
radius correction has no acdream counterpart either. Documented in-code at
the back-probe site with full citations, and pinned with
EdgeSlideBackProbePrecipiceSlideTests: a walkable polygon rediscovered near
GlobalCurrCenter, tested against GlobalSphere[0] restored to the original
failed target, crosses the edge and slides -- it does not wedge into
Collided (and the inverse case, standing inside the polygon with no edge
crossed, correctly still returns Collided matching retail's own
precipice_slide on a false find_crossed_edge).
TS-1's other two flagged gaps are real acdream-only compensating branches,
not retail reads, and get their own rows rather than being silently
retired alongside it:
- AD-53: CliffSlide's three-source reference-normal fallback chain
(LastWalkablePlane -> LastKnownContactPlane -> world-up) vs retail's
direct last_known_contact_plane.N use. A fresh read of
last_known_contact_plane's maintenance (pc:272659-272668) confirms retail
overwrites it unconditionally every validate_transition pass, including
with a steep plane -- so the fallback chain compensates for AP-4's
incomplete OnWalkable bookkeeping, not a retail-matching read.
- AD-54: the walkable-steepness reroute to CliffSlide before PrecipiceSlide
when the stored walkable polygon itself is steeper than FloorZ. Retail's
raw edge_slide has no such branch; the permissive LandingZ acceptance
that makes this state reachable IS retail-faithful (TS-4's own
BSPTREE::find_collisions citation), but whether retail's outer
transitional_insert retry loop absorbs the resulting COLLIDED_TS some
other way is not yet independently verified -- flagged open in the row.
Physics test suite: 1836 passed, 1 skipped (D4, unrelated to this change).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
acdream carried 16 status codes, curated by hand out of the CMotionInterp
and MoveToManager decompilation passes. The other 362 were unnamed, which
made every one of them a cast site waiting to happen. This slice takes the
whole table: 372 values under 378 names.
The oracle set is finally complete. All six vendored reference repos were
empty when the 2026-07-29 enum campaign ran, which is why it deferred this
decision; they are re-cloned now, so ACE's WeenieError could be read
directly instead of leaning on the UtilityBelt catalog alone.
The two agree without a single conflict. ACE has 369 members, no internal
value collisions. The catalog has 372, shares all 369 ACE names, and
disagrees on none of their values. Its three extras -- IsNowOpenFellowship
(0x050B), IsNowClosedFellowship (0x050C), LockedFellowshipCannotRecruit
(0x0518) -- each turn up in ACE's separate WeenieErrorWithString enum with
a `_` marking the interpolated name, so the catalog is just the less-split
view of the same client enum. All three are adopted on agreement between
two oracles, not on one.
Retail cannot arbitrate any of this. acclient.h has no counterpart enum;
its charError (26) is character-creation only. Recorded, not guessed
around.
Six values keep two names. acdream's NotGrounded, CrouchInCombatStance,
SitInCombatStance, SleepInCombatStance, ChatEmoteOutsideNonCombat and
ActionDepthExceeded are each anchored to a retail decompilation site, where
ACE's names for those values are server-side coinages. Rather than pick,
both are declared, acdream's first so ToString() is untouched.
Behaviour is unchanged, and there is no way for it not to be: nothing in
the tree branches on a WeenieError member. MotionInterpreter's switch is on
a motion type and merely returns one of these; WeenieErrorText.For switches
on a raw uint; the chat translation table WeenieErrorMessages is keyed on
uint throughout, so naming a code does not make it render. The one site
that moved is RemoteTeleportHook, where the (WeenieError)0x3Cu cast becomes
the now-named WeenieError.ITeleported at the same value.
Register row AP-15 is narrowed rather than retired. Its code-catalog caveat
is superseded -- an unnamed code is no longer a way for it to bite -- but
the sentences are still ACE's doc comments rather than retail's
string_table.bin, and that part stands.
The enum moved out of MotionInterpreter.cs into its own file at the same
namespace. At 372 members it does not belong inside a physics class file.
Core tests 3903 passed / 2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the two wrong enums corrected, the remaining wire-adjacent families diff
cleanly against the retail header - same values everywhere they overlap, just
fewer members on our side. This adopts the gaps.
EquipMask gains retail's eleven INVENTORY_LOC composite slot groups (acclient.h:
3193). The 32 primitive slots were already exact and stay pinned by EquipMaskTests;
what was missing were the groups the wire and the UI actually reason in - Armor,
Jewelry, ReadySlot, Weapon, WeaponReadySlot, the wrist/finger/sigil pairs, and
All. These are transcribed as literals, not derived, for the reason the previous
commit documents at length.
That transcription immediately earned itself. A type remark on EquipMask claimed
retail's CLOTHING_LOC composite "also sets bit 31, 0x80000000, which is not a
named INVENTORY_LOC primitive". It does not. CLOTHING_LOC is 0x080001FF: the nine
wear slots plus bit 27, which is the perfectly well-named Cloak slot. No
INVENTORY_LOC member touches bit 31 at all - ALL_LOC stops at bit 30. The remark
is corrected and a test now asserts the actual decomposition.
TransientStateFlags gains WaterContact (0x8) and CheckEthereal (0x100), the two
retail bits acdream's transition never declared. Neither is produced or consumed
yet; they are named so those slots cannot be quietly reused for an acdream-local
flag and then collide.
PhysicsStateFlags gains ReservedUnused1 (0x2) and ReservedUnused2 (0x2000), which
retail declares as UNUSED1_PS/UNNUSED2_PS. Same reasoning: reserved is a fact
worth recording.
AttackHeight gains Undef = 0. The three real heights are 1-based and were already
right; retail reserves 0 and the wire sends it, so it is now named instead of
arriving as an undefined cast. The numeric values are unchanged, so this renames
nothing at runtime.
Also checked and found already correct, so left alone: ObjectInfoState (matches
ObjectInfoEnum exactly, None being DEFAULT_OI), AttackType (every primitive plus
both composites - Unarmed 0x19 and MultiStrike 0x79E0 - land on retail's
literals), RadarBlipShape, RadarBehavior, MovementType, HoldKey, ParticleType, and
PhysicsDescriptionFlag. AttackType is worth calling out because the campaign's
extraction tooling reported it as a conflict; the tool reads one line per member
and had truncated a multi-line composite. The enum was fine.
RetailEnumConformanceTests grows tables for each of the above, each citing its
acclient.h line.
Core tests 3,785 -> 3,836. Full suite 9,701 passed / 5 skipped, no failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order.
Move the canonical local movement controller, body/motion managers, object clock, movement wire data, and MTS/jump/AP sender into AcDream.Runtime. Replace process skill defaults with typed Runtime character options, make graphical and direct commands borrow one autorun owner, retain the construction-time PartArray seam, and include movement in terminal ownership convergence.
Preserve the accepted pre-inbound movement/jump and post-inbound autonomous-position order while moving the exact packet/cadence fixtures into Runtime tests. Add graphical/direct parity, two-instance isolation, teardown, allocation, architecture, and divergence-path coverage.
Co-authored-by: Codex <noreply@openai.com>
Publish prepared GfxObj, Setup, CellStruct, and EnvCell collision records without retaining their parsed DAT BSP, polygon, vertex, or shape graphs. Strip temporary physics bundles at stable world commit, keep graph traversal only as an explicit test/tooling oracle, and report graph residency from actual retained fields.
Validated by 115 focused collision/streaming tests, a zero-warning Release build, and 8,413 passing Release tests with five pre-existing skips.
Co-authored-by: Codex <codex@openai.com>
Make prepared flat BSP data authoritative for gameplay while retaining the parsed graph only as an exact sampled referee. Fail production publication when collision package data is genuinely absent, keep idempotent already-cached publication valid, and move cell membership, floor lookup, camera diagnostics, and live/static shape bounds onto the flat representation.
Validated by 8,402 Release tests, a strict dense-Arwic connected gate with 46,309/46,309 exact referee matches, and graceful shutdown.
Co-authored-by: Codex <codex@openai.com>
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples.
Co-authored-by: OpenAI Codex <codex@openai.com>
Port every current containment, overlap, walkable, and six-path moving-collision query to immutable integer-indexed assets behind a graph-authoritative referee. Exact synthetic, installed-DAT, complete-resolver, and zero-allocation gates prove bit-identical behavior before connected dual publication.
Co-authored-by: OpenAI Codex <codex@openai.com>
Append strict collision/topology payloads to the existing prepared package so later physics cutover can drop parsed DAT graphs without adding a second mapping or changing traversal behavior. The full 2,232,170-key catalog is deterministic across worker counts, exact-byte aliased, corruption-isolated, and cancellation-safe.
Add immutable indexed physics and containment BSP records, exact-bit polygon and Setup payloads, separated CellStruct/topology ownership, and iterative positive-before-negative flattening. Reject malformed graphs and ranges, and prove source identity over synthetic edge cases and installed retail DAT samples without cutting production traversal over.
Mirror retail's ten-deep LIFO transition lifetime, retain all query scratch with complete reset contracts, and remove Tier-0 enum boxing without changing collision decisions. Fresh and retained engines are bit-identical across the expanded oracle, while measured transition profiles now allocate 0 bytes per resolve.
Preserve the fixed per-cell collision walk across nested registry mutations while replacing the repeated List allocation with an explicitly owned pooled snapshot. Capture cardinality once, return storage on every exit, and pin live-list mutation behavior with a focused regression test.
Co-authored-by: Codex <codex@openai.com>
Remove the legacy GameWindow apply path and make the concrete render, physics, and static publishers the only production owner graph. Serialize full-window retirement with shared-origin teleport and session boundaries so old coordinate-frame resources cannot survive into a new world or login.
Move streamed terrain/cell/building and static collision publication behind a focused update-thread owner. Preserve retail publication order while making replacement and retirement exact by logical landblock ownership, including current-cell rebasing and adjacent-seam isolation.
Co-authored-by: Codex <noreply@openai.com>
Own every borrowed PartArray frame across callbacks, version presentation state across appearance rebinding, reject recursive phase consumption, and preserve static MotionDone when only a stale visual pose is discarded.
Port CPartArray::UpdateParts minimum-count behavior: preserve the prior visual and rigid transforms for setup parts absent from a short authored AnimFrame, including the initial hydrated rest pose.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Bind queued actions and pending inventory requests to exact live incarnations, separate optimistic placement from authoritative responses, and serialize retail-style inventory ownership across UI surfaces.
Co-authored-by: OpenAI Codex <codex@openai.com>
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.
Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.
Co-authored-by: OpenAI Codex <codex@openai.com>
Match the v11.4186 CSequence transition assembly and ACE cross-reference, including direction-specific pose gates and the strict physics epsilon boundary. Add conformance tests and correct the stale research interpretation.
Co-authored-by: Codex <codex@openai.com>
Process animation completion at the retail process_hooks boundary, then run targeting, movement, PartArray completion, and PositionManager in the named UpdateObjectInternal order for local, remote, hidden, and position-less animated objects. Retire TS-42 with deterministic conformance coverage.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Keep external-container visibility tied to the signed surface gap between the player and container physics cylinders, matching gmExternalContainerUI's authored UseRadius range watcher. This prevents a valid ViewContents response from being closed at ACE's natural approach endpoint.
Port Position::cylinder_distance and ACCWeenieObject::ObjectsInRange into Core with conformance coverage for 3-D separation, overlap, mode precedence, and inclusive boundaries.
Release build succeeds and all 5,895 tests pass with five intentional skips.
Co-authored-by: OpenAI Codex <codex@openai.com>
Resolve DAT-authored particle ranges from the hardware GfxObj, apply retail distance and completed-cell visibility gates, and preserve the exact finite/infinite off-view update semantics. This removes dense-world simulation work without shortening terrain, entity, fog, or streaming distance.
Publish doorway-clipped outdoor cells through a focused frame controller, retain effect cell identity for outdoor statics, reject hidden emitters before particle-slot scans, and offer an explicit opt-in Extended particle range.
Release build succeeds and all 5,857 tests pass with five intentional skips. Retail-conformance, architecture, and adversarial review cycles are clean; connected Aerlinthe visual/performance gate pending.
Co-authored-by: OpenAI Codex <codex@openai.com>
Port retail portal viewport projection and reveal behavior, preserve outbound combat style, drive remote and local grounded movement from authored CSequence root frames, and reuse the local prepared pose so animation hooks advance once.
User-verified portal, observer movement, combat stance, and short-tap locomotion gates. Release build passed with 5,767 tests and five intentional skips.
Co-authored-by: OpenAI Codex <codex@openai.com>
Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks.
Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state.
Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration.
Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates.
Co-Authored-By: Codex <noreply@openai.com>
Add deterministic 96-owner lifecycle and renderer-resource stress gates plus an exact twelve-cycle recall/portal ownership gate. Prove zero retained records, projectiles, spatial buckets, script queues, particle/light owners, shadows, pending effects, and mesh references after churn, GUID reuse, deletion, and session reset.
Make logical teardown incarnation-specific and reentrancy-safe with lifetime epochs, atomic resource registration, generation-aware effect/teleport cleanup, projection mutation tokens, and failure-isolated visibility fan-out. Finish canonical spatial transactions before reporting observer failures and never discard superseded cleanup failures.
Synchronize architecture, roadmap, milestones, retail research, divergence bookkeeping, and durable memory. All three independent review tracks are clean; Release build and the full 5,454-pass/5-skip suite are green.
Co-Authored-By: Codex <noreply@openai.com>
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions.
Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped.
Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory.
Co-Authored-By: Codex <noreply@openai.com>
Replace the incomplete package path with one DatCollection-backed compatibility seam for PhysicsScripts and Animations. Preserve CreateBlockingParticle's inherited payload and following cursor, route every production and audit consumer through the corrected loaders, and apply retail's post-UnPack StartTime ordering.
Add exact stored-order PhysicsScriptTable upper-threshold resolution, high-byte DID and embedded-ID validation, plus live effect profiles with Setup-to-PhysicsDesc precedence across top-level and attached entity lifetimes. Keep blocking execution deferred and narrow TS-11 accordingly.
Pin synthetic malformed/cursor/order fixtures, installed-DAT blocking and recall audits, high-index IDs, IEEE boundaries, profile teardown, and ordinary decoder parity; synchronize architecture, inventory, milestones, roadmap, research, and memory.
Co-Authored-By: Codex <noreply@openai.com>