Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes.
Co-Authored-By: Codex <codex@openai.com>
Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only.
Co-Authored-By: Codex <codex@openai.com>
Parse and route the complete 0x0264 guid/fraction/valid response, send exact 0x0263 item-mana queries, and reproduce retail meter visibility plus guid-zero cancellation for mana and health selection changes. Keep the behavior in Core state and the retained selected-object controller with wire/state/UI conformance coverage.
Co-Authored-By: Codex <codex@openai.com>
Route the selected stack quantity through retail GetObjectSplitSize semantics, send exact split-to-container and split-to-ground actions, and keep the original object in place until the server publishes the newly guided stack. Cover selection scoping, wire bytes, container placement, and world drops with conformance tests.
Co-Authored-By: Codex <codex@openai.com>
Keep the horizontal thumb at its raw pointer position so both endpoints are reachable, honor the stack entry's authored right alignment, and preserve PublicWeenieDesc plural names from CreateObject through the object table. Port retail's singular s/es fallback when no plural was sent.
Co-Authored-By: Codex <codex@openai.com>
Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101.
Co-Authored-By: Codex <codex@openai.com>
Carry signed index, object id, and raw spell word losslessly through PlayerDescription, session storage, drag mutation, and AddShortcut wire serialization while keeping gmToolbarUI object-only. Retire AP-103 and record the live ACE relog persistence gate.
Co-Authored-By: Codex <codex@openai.com>
Port global toolbar use/select/create actions, migrate the collapsed Ctrl-number bindings, and route them through a focused retained-UI controller. Preserve shortcut aliases as aliases across inventory and paperdoll drops with retail's neutral/accept/reject drag states, preventing physical item moves such as unwielding an equipped helmet.
Co-Authored-By: Codex <codex@openai.com>
Port every reachable ClientUISystem cursor branch through the production DAT enum map, preserve global-default versus widget-local event ordering, and surface the registered OS fallback. Route all four toolbar stance indicators through the same combat toggle command as the key binding, with golden DAT and transition conformance coverage.
Co-Authored-By: Codex <codex@openai.com>
User-reported: logging in at a non-Holtburg position sometimes showed
stabs/scenery floating in the wrong place. Root-caused via a threading/
lifecycle trace, not inference: WorldSession transitions to InWorld
immediately after the login handshake (WorldSession.cs:608) — well
before the player's own spawn CreateObject (which carries their real
position) has arrived over the network. The streaming gate's old
condition (`!IsLiveModeWaitingForLogin || liveInWorld`) opened the
instant InWorld fired, letting the background streaming worker
(LandblockStreamer's dedicated Thread) build real landblocks using
whatever _liveCenterX/Y held at that moment — the Holtburg startup
placeholder, not a "not known yet" sentinel. Landblocks that started
building in that window bake their world offset from that placeholder
at build time; if their build was still in flight when the real spawn
arrived and recentered the world (ForceReloadWindow, which only
unloads already-RESIDENT landblocks), they finished and got applied
anyway — stale-positioned geometry landing wherever the guess put it
relative to whatever streamed in afterward with the corrected center.
Confirmed with the user this wasn't "wrong placeholder, need a better
one" — any placeholder racing against the real answer reproduces the
same bug. The fix removes the race instead: StreamingReadinessGate
.ShouldStream requires an explicit liveCenterKnown flag (true only once
the player's own spawn has been processed) in addition to liveInWorld.
Nothing streams in live mode until the real position is confirmed — no
placeholder value is ever acted on. Preserves the #106 gate-3 fix this
gate originally existed for (auto-entry waits for terrain under the
spawn; terrain streaming must not wait for chase mode in turn, or the
two deadlock) since liveCenterKnown becomes true independently of chase
mode, driven purely by the spawn packet's arrival.
The stricter render gate (GameWindow.cs:9596, hides ALL world geometry
until chase mode engages, no relaxation) already provided a partial
safety net and is unchanged — this fix stops the stale geometry from
ever being built, rather than relying on the render gate to hide it
until the reveal.
Core 2680+2skip / App 741+2skip / UI 425 / Net 385 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while investigating #189 (missing fountain/candle particles):
reverting the A7.L1 light-carrier hydration fix (9ebb2060) made the
Town Network fountain's water-spray particle work again, which didn't
fit the earlier dat-truth finding that the fountain's own entity was
never touched by that fix. Traced with ACDREAM_DUMP_ENTITY: the
fountain's hydrated entity.Id shifted between reverted (0x400007F8)
and fixed (0x40000815) builds — a 29-id delta matching the extra
mesh-less light carriers the A7.L1 fix now keeps alive earlier in the
same landblock's hydration pass.
Root cause: GameWindow's interior-entity id scheme
(interiorIdBase + localCounter, "0x40XXYY##") reserves only 8 bits
(256 values) for a landblock's ENTIRE interior static population — a
residual explicitly flagged in the #119 fix's own comment ("counter
overflow past 0xFF still bleeds into the lbY byte"). The Town Network
hub (205 cells, one landblock) already sat at 248 before A7.L1; the
light fix pushed it to 277, past the boundary. 0x40000815 decodes as
landblock Y=0x08 — NOT this dungeon's true Y=0x07 — the exact #119
cross-landblock aliasing bug, reincarnated by entity count instead of
a computation bug. EntityScriptActivator keys particle-script
instances by entity.Id directly (no landblock-hint disambiguation
unlike the #119 batch cache), so the aliased id silently broke the
fountain's script tracking.
Fix: AcDream.Core.World.InteriorEntityIdAllocator widens the counter
8->12 bits (256->4096) by shrinking the fixed class prefix from a
full byte (0x40) to its top nibble (0x4_) — verified safe against
every entity.Id classification check in GameWindow (none decode X/Y
back out, they only check thresholds/prefixes). Added a loud
one-time [id-overflow] log if a landblock ever exceeds the new
budget, so this class of bug can never hide silently again.
Core 2675+2skip / App 741+2skip / UI 425 / Net 385 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root-caused via dat-truth inspection, not inference: the A7.L1 visible-
cell scoping fix (previous commit) had zero visual effect because the
Town Network fountain room (cell 0x00070144) registers ZERO lights of
its own — confirmed directly against the dat, not assumed. The room's
one dat-authored fixture, Setup 0x02000365 (a ceiling light 5m above the
fountain, warm color, intensity 100), has a single visual part that is a
#136-class runtime-hidden marker. Flattened mesh ref count: 0. GameWindow
's per-stab hydration loop treated meshRefs.Count==0 as "doesn't exist"
and dropped the whole entity before the Setup's Lights were ever read —
so a mesh-less "light attach point" fixture, a normal AC dat authoring
pattern, could never register. Retail's light registration (add_light,
CEnvCell::UnPack) is architecturally independent of a fixture's own mesh
visibility.
Fix: track the stab's Setup.Lights.Count alongside meshRefs during
hydration; keep the entity (with empty MeshRefs — nothing to draw, still
something to light) whenever either is nonzero. Extracted the decision
into EntityHydrationRules.ShouldKeepEntity (pure, unit-tested) since
GameWindow's hydration loop isn't independently testable. Confirmed no
downstream consumer assumes MeshRefs.Count >= 1 (WbDrawDispatcher already
guards on it before any indexing).
Core 2666+2skip / App 741+2skip / UI 425 / Net 385 green. Apparatus:
Issue93TownNetworkFountainRoomLightInspectionTests (dat-truth dump,
reusable for other rooms in this class).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Town Network hub (498 registered fixtures) starved the player's own
room: BuildPointLightSnapshot's player-nearest-128 cap sorts by raw
Euclidean distance, which isn't a reliable proxy for "same room" in a
dense maze — a fixture on the other side of a wall can be geometrically
closer than the room's own torches and win the cap. LightSource.CellId
tagging and the [indoor-light] membership probe already existed from the
c500912b/#176 arc; the missing piece was a candidacy filter.
BuildPointLightSnapshot(playerWorldPos, visibleCells) now narrows
candidates to the frame's actual visible cells before the existing
dynamics-first player-nearest cap runs (cell-less lights, e.g. the viewer
fill, always included). GameWindow feeds LAST FRAME's already-rendered
RetailPViewFrameResult.DrawableCells — one frame of latency instead of
re-threading a mid-DrawInside callback, which was the exact mechanism
(c500912b) that caused the earlier #176 seam-floor flicker regression.
The distance-sort anchor stays the player, unchanged.
AP-85 updated in place (third revision) rather than adding a new row —
same underlying divergence, now with the render-visibility approximation
of retail's true DBObj-load/flush-bounded resident registry documented
alongside its residual risk (one unscoped frame on portal re-entry).
Core 2652+2skip / App 741+2skip / UI 425 / Net 385 green. Pending: user
visual gate at the Town Network fountain, and a #176 corridor-seam
non-regression recheck (Facility Hub, different landblock).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lands the fading-secret-door feature and fixes the door "flip-back" that
surfaced while testing it.
#188 — fading-wall doors (e.g. "Pedestal Weak Spot") fade their wall part
out via TransparentPartHook instead of swinging:
- TranslucencyHookSink consumes TransparentPartHook -> TranslucencyFadeManager
(per-(entity,part) linear translucency ramp; holds at End frame).
- WbDrawDispatcher: new per-instance alpha SSBO (binding 7); ClassifyBatches
takes opacityMultiplier (1 - translucency, per CMaterial::SetTranslucencySimple
0x005396f0) forcing AlphaBlend; fully-invisible parts skipped.
- mesh_modern.vert/.frag: binding-7 InstanceAlphaBuf -> vOpacityMultiplier ->
FragColor.a *= vOpacityMultiplier.
- Register AP-89: the fade multiplies sampled texture alpha, not a separate
D3D9 material alpha channel (observably identical for texture-alpha==1 surfaces).
Door flip-back fix (affected BOTH #188 fading walls AND #187 sliding doors): a
door/wall that finished opening holds a single unchanging frame, so the
uncommitted IsEntityCurrentlyMoving cache-bypass narrowing dropped it onto the
Tier-1 static cache -- which only remembers the REST pose + opacity 1.0 --
snapping it visually shut/opaque while physics stayed open. Reverted that
narrowing: every Sequencer entity stays on the per-frame path (live pose + live
fade opacity), the known-good pre-optimization behavior. The per-frame CPU cost
that narrowing chased was a Debug-build artifact -- Release is GPU-bound
(~200 fps in Sawato, measured), so the unconditional add is free where it
matters. Left a code comment barring re-introduction.
Tests: full Core suite green (2649 passed, 2 skipped). Live visual gate PASSED --
both fading-wall and sliding doors hold open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The door-swing animation rescue (GameWindow.cs:3897, for entities whose rest pose
is a static single frame but which still carry a reactive MotionTable) was gated on
an exact display-name string match. Sliding doors, gates, portcullises, and disguised
secret-passage props ("Magic Wall") all fail that check because their in-game Name
isn't literally "Door" -- so ACE's UpdateMotion for them was silently dropped forever
by the _animatedEntities.TryGetValue bail-out in OnLiveUpdateMotion.
Retail's own dispatch chain (ACCObjectMaint::CreateObject -> CPhysicsObj::
set_description -> SetMotionTableID -> CPartArray::SetMotionTableID 0x005186e0 ->
MotionTableManager::PerformMovement) is unconditionally data-driven: the only gate
for creating a motion dispatcher anywhere in that chain is "motion table id != 0" --
no CDoor class, no WeenieType switch, no name check. Production weenie data confirms
Sliding Door / Portcullis / Gate / Magic Wall all carry the identical WeenieType=Door
+ non-zero-MotionTableId shape as a plain "Door", differing only in display name.
Fix: the branch's existing `mtableId != 0` check (already computed one line later)
is now the entire gate, matching retail exactly. IsDoorSpawn deleted (dead code);
IsDoorName kept only for an unrelated diagnostic log-label filter.
Live-verified: sliding doors now animate open/closed correctly. Full regression
green (App 741 / Core 2631).
docs(#188): file the fading-wall render gap surfaced during #187's live gate
A "Pedestal Weak Spot" secret-passage door dispatches correctly (proving #187's fix
reaches it) but never visibly changes. Decoded its actual dat MotionTable directly
(0x090000F9): its open cycle carries EtherealHook + TransparentPartHook +
SoundTableHook -- a translucency-fade effect, not part-transform motion. acdream's
IAnimationHookSink documents these hook types as intended for "GfxObjMesh / renderer
state mutations" but no sink anywhere consumes them (only Particle/Lighting/Audio are
wired) -- confirmed via full-repo grep. Collision already works correctly via a
separate server-authoritative SetState wire message, independent of the animation
hook. This is feature-shaped rendering work (a per-part runtime alpha under the
mandatory N.5 bindless pipeline), not a quick fix -- filed for its own design pass.
Kept Issue188FadingDoorMotionTableInspectionTests.cs as a reusable MotionTable/hook
decoder for future "why doesn't this animate" questions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The indoor GREY flap at a top-floor connecting room. The render portal side-cull
reconstructed each doorway's "interior side" (PortalClipPlane.InsideSide) from the
cell's AABB CENTROID. For a THIN connector cell (0xF6820118, 5 render polys), the
bounding-box center falls on the WRONG side of the 0118->0116 doorway, so the eye
read as a back-portal and the forward room 0116 was culled -> the aperture showed
the fog clear color = grey.
Retail's PView::InitCell (0x005a4b70) and acdream's own PHYSICS path
(CellTransit.cs:190) both read the explicit dat PortalSide bit ((Flags&2)==0)
instead of guessing from geometry. Port the render path (GameWindow.BuildLoadedCell)
to the same bit.
Proven by a live retail cdb trace (retail draws 0116 from the 0118 root at the grey
pose; tools/cdb/issue186-connector-decider.cdb) + an offline dat diagnostic
(Issue186...PortalSide_CentroidVsDatBit_AtGreyEye): the dat bit matches the old
centroid on every portal of these cells EXCEPT the one #186 breaks, so the switch is
surgical. Full regression green (App 741 / Core 2631); the CornerFlood + Issue113
dat-loading helpers updated to the same bit confirm every real Holtburg/tower/hall
cell floods identically. Touches neither PortalSideEpsilon nor the deleted
EyeInsidePortalOpening rescue (the two DO-NOT-RETRY traps).
Live-gated: user-confirmed no grey at any camera angle; probe shows 216 root=0118
frames, 0 still grey (0118->0116 now TRV, vis=4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause (live capture #3 + code): GameWindow's per-part landblock shadow
registration used a synthetic part-id `entity.Id * 256u + partIndex` that
OVERFLOWS uint32 for class-prefixed landblock ids (0x40/0x80/0xC0...). The << 8
drops the prefix byte, so different-class entities sharing the low 24 bits
collide on ONE shadow part-id and Register's deregister-then-insert silently
overwrites one entity's collision geometry. Landblock 0xF682 had 23 such
collisions incl. the stair runs (0xF6822100 <- {0x40F68221, 0xC0F68221}, ...),
so 3 mid-staircase steps rendered but had no collision -> the player floats into
the hole and the (faithful) PrecipiceSlide wedge fires = the 'invisible wall
half-way up the stairs'.
Fix (Option A, retail-faithful): register each multi-part landblock entity via
ShadowObjectRegistry.RegisterMultiPart under its UNIQUE 32-bit entity.Id
(retail CPhysicsObj::add_shadows_to_cells 0x00514ae0 -> CPartArray::AddPartsShadow
- one object, a part array; no synthetic per-part id). New testable builder
ShadowShapeBuilder.FromLandblockBspParts decomposes each MeshRef.PartTransform to
local pos/rot/scale; RegisterMultiPart reconstructs the identical world placement.
Building shells stay excluded (building channel); the Setup cyl/sphere path is
unchanged (runs only when entityBsp==0, retail BSP-xor-cyl dispatch). Despawn is
landblock-scoped (RemoveLandblock by cell prefix), so the id change is safe.
Tests: ShadowRegistrationOverflowTests (overflow arithmetic; old scheme drops one;
RegisterMultiPart keeps both; builder). Issue185OutdoorStairsSeamReplayTests
(dat-free clean-climb pin). Core 2629 / App 741 green, 0 warnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the two-path fork in RemotePhysicsUpdater.Tick: the former Path A
(grounded PLAYER remotes advanced by the interp catch-up with ResolveWithTransition
deliberately OMITTED, per the now-retired issue-#40 premise) is gone. Every remote --
player and NPC -- now runs the SAME per-tick catch-up + sweep + shadow-follows-resolved.
Retail's UpdateObjectInternal (0x005156b0) has no player/remote fork; this is the
faithful shape. Player remotes now get terrain-Z snap (no slope staircase), wall
collision, and MONSTER collision -- previously (Path A) they skipped ALL collision.
RETAIL PvP (adversarial review caught this): two non-PK players WALK THROUGH each
other in AC (you can stand inside another non-PK player) -- they do NOT de-overlap.
The remote-player mover now carries IsPlayer|EdgeSlide (mirroring the LOCAL player at
PlayerMovementController), so CollisionExemption's PvP block exempts a non-PK pair,
exactly as retail sets IsPlayer on every object's own transition (OBJECTINFO::init
0x0050cf30) and FindObjCollisions (pc:276812) exempts it. The first 2b draft passed
bare EdgeSlide and de-overlapped players (MORE solid than retail); the 3-lens review
flagged it. PK/PKLite/Impenetrable are not plumbed onto the remote mover yet -- the
same M1.5 gap the local player carries (TS-23, extended).
#40 proven dead in code (before the gate): PlayerVsMonster_DeOverlapsAndAbsorbsTheStallBlip
drives the real ComputeOffset -> InterpolationManager catch-up (incl. the fail_count
blip-to-tail) for a player mover converging on a monster and asserts de-overlap +
maxSpike<0.30 -- the sweep absorbs the stall-blip. ConvergingPlayers_WalkThroughEachOther
proves the PvP exemption (non-PK players pass through). Path B already ran the
#40-feared config stably; #40 (May 2026) predates the CSphere/#137/#170/#171 rebuild.
Placement (HIGH, review finding 3): the player UP routing gains the SAME placement-snap
backstop Slice 1 gave NPCs (AP-87). Without it a UM-first player (RemoteMotion seeded
to the spawn pos, then a first UP in a different cell) would sweep from a stale cell ->
garbage -> the digest's invisible/misplaced-player bug. The 4 m bodyToTarget guard +
!willBeDrTicked + dist>96 snap; near placed corrections still enqueue for smooth
catch-up. Also seed Body.Position=worldPos at UP-handler RemoteMotion creation
(mirrors the UM handler :5176) for the UP-first case.
Coupled shadow edits (research finding 9): RETIRED the players-only raw-worldPos shadow
sync -- now that players run the sweep + shadow-follows-resolved, the raw sync would
re-snap a packed player's shadow into overlap each UP. Player shadows follow the
RESOLVED body via the DR-tick loop + a new player UP-branch-tail SyncRemoteShadowToBody.
Surviving player/NPC split (AP-88): the omega -- grounded PLAYERS keep the
ObservedOmega-or-seqOmega world-frame (Concatenate) fallback ("rectangle when running
circles"); NPCs + airborne keep ObservedOmega-only body-frame (Multiply). They commute
for an upright body + yaw omega, so the fork is faithful.
Register: TS-23 extended (remote-player mover PK gap); AP-86 updated (raw sync retired
for players too, Where column fixed); AP-88 added (omega fork + eval-order note).
Tests: Core 2623 / App 741 green, 0 warnings. 3-lens adversarial review + per-finding
verification (10 agents); all 6 confirmed findings addressed (2 substantive: PvP mover
flags + player placement-snap; 4 doc/cosmetic).
VISUAL GATE (acceptance test) owed by the user -- NOTE the corrected expectation:
(a) a player remote on a hill -- no slope staircase;
(b) two packed player remotes -- they WALK THROUGH each other (retail PvP), NOT
de-overlap (this corrects the design's original "players de-overlap" gate);
(c) a player remote cannot stand inside a MONSTER (new: player-vs-monster collision);
(d) remote walk/run/jump/land/turn UNCHANGED.
Handoff: docs/research/2026-07-07-184-slice2-unify-extract-handoff.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the ~690-line per-remote dead-reckoning tick out of the >10k-line
GameWindow.TickAnimations into a testable AcDream.App.Physics.RemotePhysicsUpdater
(Code Structure Rule 1). The guard in TickAnimations now calls
_remotePhysicsUpdater.Tick(rm, ae, dt, _liveCenterX, _liveCenterY); the
animation/render half stays in GameWindow.
Pure, behaviour-neutral refactor — the Path A (grounded player remotes skip the
sweep) / Path B (NPCs + airborne players run it) FORK is preserved verbatim inside
the new class. Slice 2b collapses it.
Mechanics:
- Moved into RemotePhysicsUpdater: the Tick body (verbatim), SyncRemoteShadowToBody
(now called back from the NPC UP-branch tail), ApplyPositionManagerDelta,
TickRemoteMoveTo, ServerControlledVelocityStaleSeconds, and the diagnostics.
- Injected as delegates (kept on GameWindow — they have callers outside the DR
loop): GetSetupCylinder (player cylinder + moveto/sticky radii) and
ApplyServerControlledVelocityCycle (also called from the UP handler).
- AnimatedEntity: private -> internal (matches RemoteMotion) so the extracted
class can take it by type.
Verified byte-exact: the extracted Tick body reverse-transforms (re-indent +8, undo
the 4 delegate/id substitutions) to diff-identical against the original block.
Behaviour-neutral: Core 2621 / App 741 green (unchanged), 0 warnings. No visual gate
(2a is structure only).
Handoff: docs/research/2026-07-07-184-slice2-unify-extract-handoff.md section 3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-tick remote de-overlap sweep used a hardcoded HUMAN collision sphere
(0.48 m radius / 1.835 m capsule top) for EVERY creature, so large and small
monsters de-overlapped at human spacing (register TS-46). Retail seeds the
transition from the object's OWN Setup sphere list scaled by its wire ObjScale
(CPhysicsObj::transition 0x00512dc0 -> init_sphere(GetNumSphere, GetSphere,
m_scale); ObjScale from set_description 0x00514f40).
Slice 3 (one call site, no signature change): before the Path B ResolveWithTransition
call, read the creature's Setup-derived dims via the existing GetSetupCylinder
helper -- (setup.Radius, setup.Height) x ObjScale, the same source the local player
and the moveto/sticky radii already use, consistent with the spawn-time shadow
registration's entScale -- and pass them as sphereRadius/sphereHeight. Fall back to
the human capsule when GetSetupCylinder returns (0,0) for a shapeless / unresolvable
Setup (a zero radius would degenerate the sweep). The player call site is unchanged
(the player IS the human Setup). stepUp/stepDown stay 0.4 m (retail derives those
from the Setup too -- an adjacent divergence left as-is).
Big monsters now spread wider, small ones tighter -- the de-overlap distance tracks
each creature's true radius.
Test: RemoteDeOverlapMechanismTests.ConvergingLargeCreatures_DeOverlapWiderThanHuman
(an R=0.9 pair settles ~1.8 m -- materially wider than the human 0.96 m contact --
proving the sweep de-overlaps at the radius it is given). Register: narrows TS-46
(remotes no longer human-dimmed; residual = the two-scalar reconstruction vs retail's
sphere list, plus the 0.4 m step heights). Core 2621 / App 741 green.
Research: workflow wf_e8306250-21b (3-agent read-only sweep: acdream data source /
retail init_sphere reference / minimal-edit path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packed monster remotes interpenetrate in acdream but barely in retail on the same
ACE. Retail de-overlaps them CLIENT-side: it sweeps every remote creature every
tick against neighbours' LIVE resolved positions (the collision shadow == the
resolved m_position, re-registered every moved transition step), with the server
position a gentle catch-up target (CPhysicsObj::MoveOrTeleport 0x00516330), not a
hard-snap. The collision math was already faithful; the bug was the reconciliation
(hard-snap) + the movement model (synth-velocity) + a stale shadow.
A first attempt (reverted) enqueued EVERYTHING and left the shadow at the raw
server position — it gate-failed with invisible monsters (an unplaced body blipped
over a huge distance into the sweep -> garbage pos) and the player stuck on offset
shadows. This redo fixes both root causes, mechanism-proven in a Core test first:
- NPC UpdatePosition routes through MoveOrTeleport with a PLACEMENT-SNAP: the body
is snapped to the server pos when it is not already near it (first UP, no
Sequencer to consume the queue, >96 m, or |Body - worldPos| > 4 m); only near DR
corrections enqueue. This restores the body's placement authority (no invisible
monsters). Airborne keeps the authoritative hard-snap.
- Grounded movement drives the body from the interp CATCH-UP (ComputeOffset ->
InterpolationManager::adjust_offset, REPLACE dichotomy) instead of synth-velocity
(get_state_velocity / SERVERVEL); MovementManager::UseTime runs unconditionally.
- SHADOW-FOLLOWS-RESOLVED: after each tick's sweep the collision shadow is
re-registered at the resolved body (SyncRemoteShadowToBody), movement-gated
(|Body - LastShadowSyncPos| > 1 cm). The per-UP :5669 raw-pos sync is now
PLAYERS-ONLY, so an NPC's shadow is only ever written to its resolved body ->
neighbours de-overlap against resolved bodies, the spread PERSISTS, and collision
== render (no stuck-on-nothing). Landing clears the interp queue.
Preserved: airborne path, sticky #171 (gate + StickyManager overwrite of the seeded
frame), omega, the #173 bounce, landing, the node_fail_counter watchdog, and Path A
(player remotes, untouched -- Slice 2 unifies it).
Tests (RemoteDeOverlapMechanismTests): converging pair settles STABLE at 0.86 m
(barely overlapping = the retail look) WITH the shadow-sync vs <0.40 m (full
overlap) WITHOUT it; a third test drives the REAL InterpolationManager loop and
confirms the sweep absorbs the stall-blip (no pop-into-neighbour). 2-lens Opus
review (CONCERNS) addressed: movement-gated re-flood for the town-FPS risk;
players-only :5669; the blip-absorption test.
Register: retires TS-41 (SERVERVEL synth-velocity -> catch-up), narrows TS-44 (NPC
UP unified onto the interp queue; gate kept for orientation), adds AP-86
(shadow-follows-resolved impl) + AP-87 (MoveOrTeleport 4 m/no-Sequencer placement
snap). Known residual: the de-overlap sweep uses the human sphere for the mover, so
large creatures de-overlap at human radii (TS-46; Slice 3 plumbs Setup dims).
Visual gate PASSED (user: monsters visible + spacing much better). Core 2620 /
App 741 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The former #143 flag forced every server-object light onto the D3D dynamic path (1/d attenuation, range x1.5); retail's stationary fixtures use the static calc_point_light curve (0x0059c8b0, Ghidra-verified: f=(1-d/range)*intensity*wrap/d^3 beyond 1m, range=falloff*1.3, per-channel colour clamp) - the dynamic path burned every Facility Hub lamp ~10x hotter than retail at 3m, producing the saturated magenta wash that zebra-striped normal wall geometry into the #176 'stripes/triangles' pattern (VSync-on test + clean captures characterized it as STATIC over-bright content, not flicker/tearing/camera). The shader's static branch already implements the verified curve faithfully (mesh_modern.vert pointContribution - its wrap constants pin LIGHT_POINT_RANGE=0.75), so the whole fix is the registration decision. Site-A lights are all stationary today (AP-44); genuinely moving lights re-earn isDynamic when they exist. Suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ApplyLoadedTerrainLocked registered every static Setup light per apply, and RegisterOwnedLight appends unconditionally. A landblock can RE-apply without an unload in between (#168 ForceReloadWindow, Far->Near promotion) and static entity ids are deterministic, so each re-apply stacked another copy of the same lights under the same owner - the [seam-ent] x2->x4 growth, invisible to the identity-keyed [seam-blk] probe (stacked copies share one owner identity while intensity multiplies). The wash intensifies over the session and the stacked exact-tie copies destabilize the 128-cap pool selection sort. Fix: UnregisterOwner(entity.Id) before the per-entity register loop - re-apply idempotent, first apply a no-op. The live-spawn path keeps its existing guid-dedup teardown (GameWindow:3134 -> :4679). Suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seam-floor purple flicker was NOT a draw z-fight. The in-engine
[seam-*] probe (ACDREAM_PROBE_SEAMDRAW - built because RenderDoc cannot
capture this pipeline: it hides GL_ARB_bindless_texture and the
mandatory-modern startup gate throws; AMD GPU rules out Nsight) killed
every double-draw suspect: ONE shell instance per seam cell at the
lifted z, no floor-coincident entity (portal entities sit at z=-12.05),
zero portal depth fans in the sealed Hub. What it caught instead: the
corridor floor's applied light set flipping wholesale with the flood.
Root cause: c500912b scoped BuildPointLightSnapshot by the per-frame
portal flood, on the research doc's gloss of CEnvCell::visible_cell_table
as "the portal-flood visible set". The named decomp refutes the gloss:
add_visible_cell (0x0052de40) DBObj-LOADS absent cells and inserts them;
a cell activation adds itself + its whole dat visible-cell list
(0x0052e228/0x0052e24a); entries leave only via the flush machinery.
It is the RESIDENT-cell registry - gaze can never remove a cell.
add_dynamic_lights (0x0052d410) walks the WHOLE table per frame
(caller 0x00452d30), and insert_light (0x0054d1b0) caps the pool by
distance to Render::player_pos (0x0054d1dd). Retail's pool is a function
of player position only. Ours followed the camera: turning changed the
flood (probe: 8..41 cells across one turn), the six intensity-100
under-room portal purples entered/left the pool, and the wedge blinked.
Fix: BuildPointLightSnapshot(playerWorldPos) collects ALL registered
(=resident) lit lights; over cap keeps dynamics FIRST (retail's separate
7-slot dynamic pool never competes with statics) then nearest-the-player;
the RebuildScopedLights callback is deleted. Live-verified with the probe:
full-circle turn, flood churning 8..41, the floor set held the same 8
identities on every post-spawn frame. The purple wedge SHAPE stays - it
is cdb-proven retail-faithful.
Residual deviation (AP-85 rewritten): single 128 pool vs retail's
7-dynamic/40-static degrade-scaled dual pools - the Hub now shows
7 purples + viewer where retail's cdb showed 4 + viewer + fixture slots;
if the gate reads the wedge as too purple, the A7 dual-pool cap is the
faithful trim.
Pins: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(rewritten to the corrected model),
PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics,
PointSnapshot_OverCap_KeepsNearestThePlayer,
PointSnapshot_ResidentCollection_CellTagDoesNotFilter.
Suites: Core 2599+2skip / App 726+2skip / UI 425 / Net 385.
The [seam-*] probes stay until the visual gate passes, then strip.
Correction banner added to 2026-07-06-a7-per-cell-lighting-pseudocode.md;
outcome banner on the z-fight handoff; ISSUES #176 updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port retail's per-frame light collection: the point-light pool is built from ONLY the
currently-visible cells' lights, matching CObjCell::add_*_to_global_lights
(0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a
flat world-space set capped at 128-nearest-camera.
- LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both
registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics).
- LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff
CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting
backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8).
- Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the
flood resolves prepareCells and before the draws (renderers select from the same
in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass).
- [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION.
Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant.
CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe
PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame,
CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool
composition is NOT the cause. #176 real cause = an over-bright purple point light
(intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss
(stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the
camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected;
ISSUES #176/#177 re-diagnosed; render digest banner updated.
Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light
0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md.
Suites green: Core 2595 + 2 skip, App 719 + 2 skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the safe-batch (implementer finished this but died before
committing; coordinator independently verified bit-identity + safety
post-interruption). Two per-frame HashSet<uint> allocations replaced by
reused cleared-in-place fields:
- GameWindow._animatedIdsScratch: WalkEntitiesInto treats null and empty
identically (line 682 `is null || Count == 0`), so an always-non-null
empty set == the old null-when-empty local. Verified.
- RetailPViewRenderer._drawableCellsScratch: flows into
RetailPViewFrameResult.DrawableCells (live ref) but every consumer reads
it same-frame; sigDrawableCells is a per-frame OnRender local that
EmitRenderSignatureIfChanged only FORMATS to a string (no reference
retention, no _lastSig set field). Built frame N -> read frame N ->
cleared frame N+1. No cross-frame corruption.
Build green, full suite 4116 passed / 4 skipped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AnimationSequencer.Advance() allocated a fresh PartTransform[partCount]
every call (BuildBlendedFrame/BuildIdentityFrame), and GameWindow.
TickAnimations reassigned entity.MeshRefs to a fresh List<MeshRef>(partCount)
every tick for every animated entity - including idle NPCs on a breathe
cycle. Both fire every frame in steady state and both become garbage
immediately after being consumed.
Fix: cache a PartTransform[] sized once in the AnimationSequencer
constructor (_setup is readonly and never reassigned, so partCount is
fixed for the sequencer's lifetime) and overwrite it in place each
Advance() call. Cache a List<MeshRef> on the long-lived AnimatedEntity
record (one per animated entity, held in GameWindow._animatedEntities)
and Clear()+refill it each tick instead of allocating a new list.
Verified safe: TickAnimations runs single-threaded to completion before
any draw-side consumer reads MeshRefs (WbDrawDispatcher re-reads
entity.MeshRefs fresh every frame, keyed by entity.Id - it never caches
the list reference across frames). The only place that copies MeshRefs
across an animation boundary (RefreshPaperdollDoll) takes an explicit
`new List<MeshRef>(pe.MeshRefs)` value copy; MeshRef is a readonly
record struct so that copy is unaffected by later in-place mutation of
the source list. No test holds two Advance() results simultaneously.
dotnet build clean, dotnet test 4120 passed / 4 skipped (unchanged).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 'run into the last corridor window and pop up through its roof'
report: the live callers passed sphereHeight: 1.2f into
SpherePath.InitPath, whose head-sphere formula (height - radius) put the
head sphere center at 0.72 - the capsule top at 1.2m. The top 0.63m of a
1.83m character had NO collision, so at the corridor-end window alcove
(0x8A020179 -> 0x8A02017E: 0.70m sill face, 1.3m opening, sloped funnel
behind) the step-up's placement never saw the head overlapping the
lintel solids and let the player climb in head-through-roof.
Dat truth (HumanSetup_CollisionSpheres_DatTruth): Setup 0x02000001
spheres = (0,0,0.475) r=0.48 and (0,0,1.350) r=0.48 - capsule top 1.83 =
Setup.Height 1.835. Retail collides with that sphere list verbatim
(CPhysicsObj::transition 0x00512dc0 -> init_sphere(GetNumSphere,
GetSphere, m_scale)).
Fix: PlayerMovementController + the GameWindow remote resolve now pass
sphereHeight: 1.835f (capsule top; head center 1.355 vs dat 1.350).
InitPath unchanged - captured-input replay fixtures (recorded 1.2
inputs) stay byte-identical. Register TS-46: the (radius, capsule-top)
scalar approximation of the Setup sphere list (5mm foot/head offsets;
remotes use human dims) with the retire path (plumb the sphere list).
Pins: WindowOpening_HeadCannotFit_EntryBlocked (22-frame walked approach
wall-slides at the sill, never enters 0x8A02017E) +
WindowAlcove_RaisedPlacement_HeadInLintelSolid_Collides (Path-1
placement rejects the raised head in the lintel solids) + the
WindowShaft_FullPolyDump / HumanSetup dat inspections.
Suites: Core 2562 / App 713 / UI 425 / Net 385, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GameWindow gets one field (_frameProfiler), one FrameBoundary() call at
the top of OnRender, three stage scopes (Update in OnUpdate, Upload
around _wbMeshAdapter?.Tick(), ImGui around _imguiBootstrap.Render()),
and one Dispose() call in teardown before _dats?.Dispose() releases the
GpuFrameTimer query ring. No new feature bodies land in GameWindow.cs —
all profiler logic lives in AcDream.App.Diagnostics.
DebugVM.FrameProf mirrors RenderingDiagnostics.FrameProfEnabled so the
DebugPanel checkbox ("Frame profiler ([frame-prof])") toggles the 5s
report live. Note: the checkbox lives in
AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs (IPanelRenderer,
backend-agnostic) alongside the other Diagnostics-section checkboxes —
not in AcDream.UI.ImGui, which holds no panel-drawing code per Code
Structure Rule 3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The shipped derivation looked up mt.Cycles[DefaultStyle]; the dat
Cycles dictionary is keyed by the COMBINED (style << 16) | substate
word (CMotionTable.cs:683), so the lookup always missed and the pose
override silently fell back to placement frames — user re-test: "175
is not fixed". The pins covered the override plumbing but not the
derivation, the one part with no offline fixture.
Extract the derivation to Core as MotionTablePose.DefaultStatePartFrames
using the retail SetDefaultState chain (StyleDefaults[DefaultStyle] ->
combined-key LookupCycle, same wrap arithmetic as CMotionTable.cs:683)
and pin it against the REAL dat (human MT 0x09000001 resolves a
34-part pose — this test fails on the old key math). Short poses now
apply PER PART (ShadowShapeBuilder already falls back per index) so a
door anim posing only the panels still overrides them while the
BSP-less header keeps its placement frame. [shape-pose] diagnostic
(ACDREAM_DUMP_MOTION) prints mt id + resolved part0 pose per BSP
registration so live launches show the actual outcome.
Suites: Core 2540 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Facility Hub double door (Setup 0x02000C9D) embeds the player into
the visual panel from one side and blocks with a phantom slab on the
other: its Setup PLACEMENT frames pose the two panels AJAR (yaw
-150/-30 deg, 0.44 m behind the doorway plane — dat-confirmed by the
Issue175 inspection) while the rendered door poses them CLOSED from
the wire-supplied motion table via the sequencer. ShadowShapeBuilder
read placement frames, so the 1.66x0.29x2.95 m physics slabs
registered at the ajar pose. Retail tests each part's LIVE
CPhysicsPart pose — for an idle door, the motion table's default
(closed) state.
Fix: ShadowShapeBuilder.FromSetup gains partPoseOverride (BSP part
shapes only); RegisterServerEntityCollision derives it from the spawn
MotionTableId via GameWindow.MotionTableDefaultPose (default style ->
first cycle -> LowFrame part frames). Null/short poses fall back
per-part to placement frames — table-less entities and landblock
statics unchanged. One-shot snapshot vs retail's per-frame live pose
is register row AP-84 (equivalent for the door lifecycle: closed ==
default pose; open == ETHEREAL bypasses collision, #150).
Pins: FromSetup_PartPoseOverride_ReplacesPlacementFrames /
_NoOverride_KeepsPlacementFrames / _ShortOverride_FallsBackPerPart.
Suites: Core 2539 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail CPhysicsObj::RemoveLinkAnimations (0x0050fe20) is a tailcall to
CPartArray::HandleEnterWorld (0x00517d70) -> MotionTableManager::
HandleEnterWorld (0x0051bdd0): remove_all_link_animations PLUS a full
pending_animations drain (while (head) AnimationDone(0)), each pop
relaying MotionDone so CMotionInterp pops its pending_motions node in
lockstep. acdream bound the seam to the bare sequence strip, so every
jump's LeaveGround removed the animations that queued manager nodes
were counting down on — orphaning them (NumAnims>0, anims gone) and
permanently damming BOTH queues. MotionsPending() then never drained
(probe round: last player pending=False at the first MovementJump
press; old jump motions still completing at rest minutes later) and
BeginTurnToHeading/BeginMoveForward's verbatim motions_pending gates
starved every armed moveto: ACE's mt-6 walk-to-door armed but the body
never walked (wire-proven, seqs 98-101); the close-range use turn
never completed so the deferred action was silently eaten. Doors only
worked on a fresh session (shallow queue).
Rebind both production sites (remote EnsureRemoteMotionBindings +
the player's EnterPlayerModeNow block) to Manager.HandleEnterWorld();
the sequencer wrapper was a pure passthrough so the manager call is a
strict superset. All six interp seam sites (LeaveGround, HitGround,
Dead, and the detached-object guards) are the same retail chain.
Harness mirrors updated; pins: Issue174LinkStripDrainTests (the seam
drains both queues; fresh motions queue and complete after). Suites:
Core 2535 / App 713 / UI 425 / Net 385 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An observed character jumping into a dungeon ceiling hovered at the
roof until its ballistic arc decayed, landing visibly late (user
report, 0x0007). The remote DR tick sweeps collision (position pinned
at the ceiling — no clip-through) but retail's post-transition velocity
response, CPhysicsObj::handle_all_collisions (pc:282699-282715:
v -= (1+elasticity)*dot(v,n)*n), was only ported for the LOCAL player
(L.3a). The remote body kept its +Z launch velocity and re-integrated
it into the roof every tick — the position was clamped but the
timeline was pure ballistics.
Retail runs handle_all_collisions after every SetPositionInternal for
every physics object, remotes included. Mirror the local reflection
block in the remote sweep's post-resolve path: same formula, same
AD-25 airborne-before-AND-after suppression (corridor slides and
landings don't reflect; the landing snap's Velocity.Z <= 0 gate stays
intact), same Inelastic zero-out for future missiles. AD-25 register
row extended to cover both sites.
Suites green: Core 2533 / App 713 / UI 425 / Net 385.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structural capstone of the R5 movement-manager arc; zero behavior change.
Retail MovementManager (acclient.h /* 3463 */, 16 bytes / four pointers)
gives every CPhysicsObj ONE owner for its motion_interpreter +
moveto_manager. acdream carried them as loose per-entity objects wired by
hand at three sites. This slice:
- New src/AcDream.Core/Physics/Motion/MovementManager.cs — owns
MotionInterpreter + lazy MoveToManager (MakeMoveToManager 0x00524000 via
a MoveToFactory closure, the acdream stand-in for the physics_obj/
weenie_obj backpointers) + the relays with retail call shapes:
PerformMovement 0x005240d0 (types 1-5 -> minterp, 6-9 ->
MakeMoveToManager + moveto, (type-1)>8 -> 0x47), UseTime 0x005242f0
(moveto only), HitGround 0x00524300 (minterp FIRST then moveto),
HandleExitWorld 0x00524350 (minterp only), CancelMoveTo 0x005241b0,
HandleUpdateTarget 0x00524790, IsMovingTo 0x00524260.
- RemoteMotion.Movement + PlayerMovementController.Movement hold the ONE
facade; Motion/MoveTo become child views so the comment-dense call sites
read unchanged. The three wiring sites (EnsureRemoteMotionBindings,
EnterPlayerModeNow, the chase harness — same commit per the mirror rule)
construct through MoveToFactory + MakeMoveToManager(), preserving the
pre-facade eager timing (side-effect-free ctor = unobservable either way).
- Relay call sites repointed: both remote landing HitGround pairs + the
player landing pair, despawn HandleExitWorld, TickRemoteMoveTo + the
player Update UseTime, RouteServerMoveTo (takes the facade; routes via
the retail PerformMovement dispatch), InstallSpeculativeTurnToTarget,
host HandleUpdateTarget/InterruptCurrentMovement closures (retail
CPhysicsObj::HandleUpdateTarget @0x00512bf0 fan head + the TS-36
interrupt chain, now the literal facade relays).
- NOT absorbed per the slice spec: unpack_movement stays App
(RouteServerMoveTo + UM heads; Core.Net types stay out of Core.Physics);
TS-42 per-tick order untouched (R6); #170/#171 gate-passed machinery
untouched. PerformMovement's set_active(1) head not re-asserted (spawn
asserts Active; status quo — no new register row).
- Register: TS-41/TS-42 source wording freshened to the facade shape;
AD-36 retire note corrected (facade half closed; residue = entities
that never get a RemoteMotion). No new rows.
- Conformance: 15 new MovementManagerTests pin the dispatch table, lazy
create, relay targets/order, null tolerance. Suite 4052 green; the
183-case/funnel/moveto/chase/sticky suites UNMODIFIED (harness
construction mirrors production, test bodies untouched).
Decomp: docs/research/2026-07-03-r5-managers/r5-movementmanager-decomp.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three unpack_movement parity items (facade deferred per the handoff's
own optional clause — see r5-wiring-handoff §V4 status):
1. HEAD style-on-change (0x00524440 @00524502-0052452c): both GameWindow
routing heads (remote + player) now dispatch DoMotion(style, ctor
defaults) when the UM's stance differs from the interp's current
style, BEFORE the movement-type routing — for EVERY type. Previously
style applied only through the mt-0 funnel copy, so a chase/turn UM
(mt 6-9) carrying a stance change started the move in the OLD stance
(a monster charged in NonCombat posture until the next mt-0). The
RetailObserverTraceConformanceTests exclusion note updated — the
trace filter stays (head calls can't appear in a
MoveToInterpretedState replay) but the production gap it pointed at
is closed.
2. #164 (closes): the action-replay loop threads each action's autonomy
into the dispatch params (Autonomous = the 0x1000 splice, raw
305982) — replayed actions enter the interpreted actions list with
their real autonomy instead of ctor-default false.
3. mt-0 wire flags consumed (UpdateMotion parsed them since R4-V3):
0x1 StickToObject → CPhysicsObj::stick_to_object port (0x005127e0:
resolve target, PartArray radii — 0 when shapeless, guid-as-is for
acdream's flat entity table — → PositionManager.StickTo; unresolvable
target → no stick), at BOTH case-0 tails in retail order
(@00524583-0052458e: funnel apply → stick → longjump flag);
0x2 StandingLongJump → Motion.StandingLongJump, UNCONDITIONAL write
(absent flag clears — retail @0052458e). ServerMotionState gains the
StandingLongJump field.
Conformance: ChaseArm_WithStanceChange_AppliesStanceBeforeTheChase
(harness mirrors the routing-head dispatch) +
Actions_ReplayCarriesAutonomyIntoTheInterpretedList. Suite 4041 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate 1 (2026-07-04): "better in general" + three residuals — monsters
pushed INTO the player, attacks with stale facing, position
"flashing/flapping instead of gliding". All three are ONE mechanism:
the legacy NPC UpdatePosition handler hard-snaps position, orientation,
and velocity/cycle UNCONDITIONALLY, fighting the armed sticky every UP
(ACE's authoritative rest pose sits ~0.6 m out and its server-side
facing lags the strafing target; the client stick holds 0.3 m + live
facing — oscillation at UP cadence).
Retail is immune by architecture, not by tuning: UP corrections flow
through the InterpolationManager into the SAME per-tick
PositionManager::adjust_offset chain where StickyManager::adjust_offset
OVERWRITES them while armed (0x00555190 chain order; 0x00555430 assigns
m_fOrigin). A server correction can never fight an armed stick
frame-by-frame. The remote-player branch already has exactly that
(queue -> combiner -> sticky overwrite); the legacy NPC path snaps
outside the chain.
Fix: suppress the NPC UP position/orientation/velocity-adoption snaps
while the entity is stuck (PositionManager.GetStickyObjectId() != 0) —
the retail chain semantics translated to the snap architecture.
LastServerPos/Time + cell bookkeeping still record; server truth
reasserts on the first UP after unstick, bounded by the 1 s sticky
lease. Register row TS-44 (same commit); retires with the S6/R6
interp-queue unification of the NPC path.
Apparatus: ACDREAM_PROBE_STICKY=1 — per-guid [sticky] lifecycle lines
(STICK / UNSTICK / LEASE-EXPIRE / TARGET-status teardown), per-armed-
tick steer lines (signed gap dist, applied delta, heading delta, live
resolve), and [sticky-snap-skip] at the suppressed-snap site.
PhysicsDiagnostics.ProbeStickyEnabled owns the flag (rule #5).
Full suite 4038 green. Awaiting gate 2 (pack melee vs retail).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Group-melee interpenetration + facing drift: the R5-V1-ported
StickyManager/PositionManager were Core-only — the StickTo/Unstick seams
were unbound and every arrival radius was 0, so ACE's Sticky|UseSpheres
melee chases closed ~one body-radius too deep and froze at stale arrival
poses until the next wire re-arm. Retires TS-39.
Wiring (anchors re-verified against the named decomp this session):
- EntityPhysicsHost owns a PositionManager; HandleUpdateTarget fans
MoveToManager then PositionManager (CPhysicsObj::HandleUpdateTarget
0x00512bc0 order).
- Seams bound remote + player: MoveToManager.StickTo (BeginNextNode
sticky arrival @0x00529d3a), Unstick (PerformMovement head), and
MotionInterpreter.UnstickFromObject (UM funnel head, 0x0050eaea).
- AdjustOffset at the retail UpdatePositionInternal slot (@0x00512d0e):
NPC branch composes pre-sweep (steer is swept by ResolveWithTransition);
remote-player branch chains the combiner offset through the shared
delta frame (the interp stage) so sticky OVERWRITES when armed
(0x00555430 assigns m_fOrigin, not accumulates); player inside the
30 Hz physics quantum before UpdatePhysicsInternal.
- UseTime (the 1 s lease watchdog) at the UpdateObjectInternal tail
(@0x005159b3): unconditional per remote; player gated on the physics
tick (retail's MinQuantum gate skips UseTime too).
- Real setup cylsphere radii (CPartArray::GetRadius/GetHeight
0x005180a0/0x005180b0 = setup radius/height x ObjScale from the spawn
record): own via EnsureRemoteMotionBindings + player wiring; target via
RouteServerMoveTo AND the speculative use-walk install (retail resolves
the target PartArray at EVERY MoveToObject site — ACE PhysicsObj.cs:951).
- Teardown parity: exit_world (0x00514e60) UnStick + ClearTarget before
the ExitWorld notify; player teleport fires teleport_hook's tail
(UnStick in SetPosition + EntityPhysicsHost.NotifyTeleported =
ClearTarget + NotifyVoyeurOfEvent(Teleported) @0x00514f1b) so mobs
stuck to the player drop their sticks on a recall.
- SERVERVEL arbitration also yields to a stuck entity (same starvation
class as the #170 fix — sticky owns the between-snap translation).
- StickyManager.UseTime aligned to retail's strict > deadline
(0x00555626; ACE >): two V1 tests had pinned the >= edge — corrected.
Register: TS-39 deleted; TS-41 narrowed (stickyArmed gate); TS-43 added
(remote teleport_hook gap — self-corrects within the 1 s lease); AP-23
narrowed (real radii at the speculative site; only the use-radius
buckets remain invented).
Conformance: 2 new full-stack sticky scenarios in
RemoteChaseEndToEndHarnessTests (arrive -> stick -> strafing-target
gap+facing track -> lease expiry; unstick-on-rearm -> re-stick).
Full suite 4038 green.
Pre-commit adversarial diff review (3 lenses + per-finding refuters)
confirmed and fixed 4 findings: ObjScale-dead radius read, player
UseTime order inversion, missing teleport voyeur notify, speculative-
site radius asymmetry.
Awaiting the user visual gate: pack melee side-by-side vs retail
(attackers reshuffle + keep facing; some overlap is ACE-server-side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>