docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §3, §6
Step 6. Corrects two things in the original AD-25+AP-7+TS-4 framing:
AD-25's local-player half was already ported by the #182 rebuild
(2026-07-07) and the remaining gap is remote/NPC-only (Campaign P P3
scope); and no client-side PhysicsState.Sledding auto-toggle exists
anywhere in the named-retail decomp or ACE's PhysicsObj.cs -- the only
Sledding write site in any reference repo is a per-weenie game-data
property, not a physics landing response, so this issue must not wait on
inventing one.
AP-7 landed this session. TS-4's removal was attempted per its own
fixture-first requirement and reproduced the historical 2026-04-30 wedge,
so it stays deferred (see its register row and the research doc's §7 item
6). Closure pends TS-4 actually landing and a fresh capture against the
campaign's final visual-matrix item 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P2 step 2-3
(docs/research/2026-07-30-response-layer-edge-family-pseudocode.md §4, §6
Step 3). Per the research doc's own port order, TS-4's Path-6 steep-poly
shortcut may only be removed after a fixture reproduces the original
"stuck in falling animation on a steep roof" symptom cleanly with the
shortcut disabled. No surviving live-session fixture exists from the
2026-04-30 L.4 commit (b1af56e); this adds a dat-free multi-frame capture
(Ts4SteepRoofWedgeCaptureTests) using BSPStepUpFixtures.SlopedUnwalkable's
63.4 degree slope, replayed at 30 Hz with gravity integrated between
PhysicsEngine.ResolveWithTransition calls -- the same idiom as
Issue185OutdoorStairsSeamReplayTests.
Against today's baseline (shortcut active) the capture is green, as
expected (the shortcut's explicit AddOffsetToCheckPos keeps the body
moving every tick by construction).
Scratch-removed the shortcut (both BSPQuery.cs sphere0/sphere1 branches,
not committed -- reverted after capture) and re-ran the same test: the
body falls and lands cleanly on the steep polygon at tick 17 (InContact,
OnWalkable=false, via retail's own permissive CTransition::check_walkable
LandingZ gate, pc:273202), then freezes at that exact position for the
rest of the run -- the exact historical wedge shape, tripping the test's
own >0.5s-frozen threshold at tick 33.
Root-cause diagnosis via ACDREAM_DUMP_EDGE_SLIDE=1: the freeze is upstream
of EdgeSlideAfterStepDownFailed/CliffSlide entirely (none of that
dispatch's diagnostics fire). TransitionalInsert's Phase 2 object-collision
check returns Adjusted on every retry attempt because Path 6's retail-
faithful SetCollide returns ADJUSTED_TS without repositioning the sphere
(unlike the interim shortcut, which explicitly pushes the sphere off the
face) -- the same steep polygon re-triggers Path 6 on the immediate retry,
forever, and Phase 3 (the sp.Collide handling that contains DoCheckWalkable,
the Placement re-test, and the TS-1 CliffSlide chain) is gated on Phase 1
AND Phase 2 both returning OK, so it is structurally unreachable from this
state. TS-1's completeness is moot here -- the code path that would call
into it never runs.
Per the mission's explicit escape valve: STOP here, keep the shortcut, and
report -- do not improvise a third variant. Full diagnosis, the exact
capture, and the concrete next research question (does retail's own
transitional_insert loop check sphere_path.collide on every iteration
regardless of Phase 2's own return value, or only when Phase 2 returns OK?)
are recorded in the research doc's §7 item 6 and the doc's headline; the
campaign plan's P2 section gets a matching status note.
Physics test suite: 1841 passed, 1 skipped (D4, pre-existing/unrelated), 0
failed.
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>
AP-7's gate is the Sledding branch; ACE's linear calc_friction (0.25 dot
threshold, unconditional small-angle subtraction, Sledding overrides) is
the correct reading and the L.3c walking regression is architecturally
moot for the root-motion path. TS-1's register cite is stale dead code -
the PrecipiceSlide/CliffSlide/EdgeSlide chain is substantially ported
with one precise back-probe re-cache gap. #166 is a composite of
AD-25+AP-7+TS-4, not a missing Sledding auto-toggle (no client write site
exists). TS-4 removal is sequenced AFTER the TS-1 gap closes with
captured fixtures. #116 stays oracle-first. Port order + 8 open
Ghidra-verify questions recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one user stop of the campaign: each scenario names its setup, the
retail-correct outcome, and the register rows / issues it closes,
including the stale #172-#175/#41 gate reconciliation via scenario 8 and
the #167 leash check riding scenario 12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Outbound 0xF61C requires the published movement controller, so the login
seed ran; the residual suspect is a seeded (cell,pos) pair the resolver
cannot operate on. PhysicsEngine.DiagnosticLog has no production
assignment, so the #111 [snap] lines were structurally absent from the
Coldeve log - wiring it is a P6 prerequisite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zero landblock loads occurred before the login recenter (worker gated
until the real spawn center), so no stale Holtburg-frame physics blocks
ever existed. Remaining: (f) login SnapToCell seed race -> NO-LANDBLOCK
verbatim resolves, (e2) CellGraph/_landblocks skew, (g) root-motion Frame
not reaching the transition. The probe run's [resolve] line pattern
discriminates all three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Log facts: outbound MTS/AP flowed all through the run-on-spot window;
reveal collision=True is attested by the SAME _landblocks dict the
resolver walks; the 'unattributed' recenter is the default Holtburg
pre-login center -> first real position. Deduction: local display is
client-authoritative, so ACE rejection cannot pin the local body - the
defect is local zero-advance resolves. Prime suspect: login recenter may
not route through Slice E generation retirement, leaving stale
Holtburg-frame neighbor landblocks overlapping the new frame (#145
stale-offset class, neighbors were explicitly left by the 2026-06-20
center-only fix).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-06-21 residual predates its own fix: AD-30's verbatim hold +
the #145 carried anchor kill the pick march, R3-W6 StopCompletely kills
the stale arrival velocity, canonical outbound position ownership kills
the 17410 wire artifact class, and the reveal barrier holds incomplete
destinations in the tunnel. Pinned by TeleportFarTownRunawayTests
(south+east unstreamed-edge); connected evidence: 20-teleport Coldeve
session 2026-07-29 + K3/K4 portal routes. Carried-debt lists updated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Byte-decoded GetStart/MaxConstraintDistance (0x0050ebc0/0x0050ec10) from
the PDB-paired v11.4186 binary: start = outdoor 10 m / indoor 5 m, max =
outdoor 50 m / indoor 20 m; the player-vs-remote branch is vestigial
(identical constant pairs). ACE's start mapping is inverted - do not
copy. Full SmartBox::HandleReceivedPosition 0x00453fd0 arming flow
transcribed (remote self-anchor post-MoveOrTeleport, player anchors to
received position, teleport branch zeroes velocity). TS-35 + #167 retire
together at the P5 port.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TS-46: init_sphere 0x0050c670 seeds the Setup's own sphere list; step
heights come from setup step_up/step_down x scale (0x005180d0/f0) and the
local player already ports this (PlayerModeController.ApplyStepHeights) -
remote/ordinary 0.4f pins are a plumbing gap. AD-25: handle_all_collisions
0x00514780 is one uniform CPhysicsObj function; the remote reflect block
should swap to the existing PhysicsObjUpdate.HandleAllCollisions port.
TS-23: parse/storage/exemption machinery exists; only moverFlags call
sites read a GUID heuristic. AP-71: check_entry_restrictions 0x0052b6d0
transcribed; restriction_obj write-site field collision flagged OPEN
(Ghidra MCP unreachable). AP-10: ValidateWalkable verbatim-correct; only
the dry-corner constant collapsed, plus WATER_CONTACT_TS declared but
never written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The issue's premise (HasOmega cleared, pi/2 fallback) was disproved by
the R6 complete-root-frame cutover: MotionTable 0x09000001 authors
omega.Z = -1.5 rad/s literally. The run turn multiplier is the verbatim
FUN_00527be0 port (RunTurnFactor = 1.5). No cdb capture needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
User-directed pre-vendor detour from the 2026-07-29 physics audit. Goal:
Retail Movement Parity v1 - zero physics TS rows, no unargued
feel-affecting AP rows, issues #262/#165/#166/#116/#167/#72/#153 closed,
one batched connected visual matrix. Sonnet implements, Opus reviews at
slice boundaries. Roadmap gains the Campaign P entry and records Campaign
N's user-accepted closure; CLAUDE.md current-state pointer updated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Slice 4 passed its two-client Coldeve visual gate and was user-accepted;
world-interaction program resumes at Slice 5 (vendor browsing) after the
physics parity campaign.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user reported crystal shards hovering in the air above every Bind
Stone on Coldeve (setup 0x020010AC) that do not exist in the retail
client. The DAT truth, extracted with the new tools/SetupInspect probe:
the model authors SEVEN parts - pedestal, spinning column, inner
crystal, and four shard meshes parked in a static ring at Z=3.0 in the
placement frame and every frame of the idle cycle - and frame 0 of that
idle cycle fires four TransparentPartHooks (parts 3-6, start=end=1.0)
each loop. Retail hides the shards through those hooks; the model
simply ships with permanently-hooked-invisible parts.
acdream's hook chain was intact end to end - the static-animating
workset captures the hooks (RetailStaticAnimatingObjectScheduler ->
AnimationHookFrameQueue -> TranslucencyHookSink), and
TranslucencyFadeManager committed translucency 1.0 for parts 3-6 -
but BOTH dispatchers' bare-GfxObj branch read the fade with a
hard-coded part index 0 under a false #188-era assumption ("a bare
GfxObj entity has exactly one part"). Every live server object is a
FLATTENED multi-part entity in exactly that branch: SetupMesh.Flatten
emits one bare-GfxObj MeshRef per Setup.Parts[i], order preserved,
AnimPartChanges replacing in place - so the MeshRef ordinal IS the
retail CPartArray ordinal TransparentPartHook.PartIndex addresses.
The committed invisibility for parts 3-6 was never consulted and the
shards drew forever. Proof the ordinal was trustworthy all along:
click-selection in the same loops already publishes it as the part
identity (Slice 4 picking runs on it in production).
Fix: both the legacy classifier and the packed oracle now pass the
per-part ordinal (partIdx / packedPart.PartIndex) to the translucency
lookup. Single-part objects still read index 0; the #188 door fades
are unchanged; the Setup-expanded branch already indexed correctly.
Any other object hiding authored parts via idle-loop hooks gets its
retail appearance from the same change.
tools/SetupInspect is the new reusable DAT probe that cracked this:
dumps a Setup's parts, parent indices, GfxObj vertex bounds, placement
frames, motion-table default cycle, sampled animation frames, and all
animation hooks.
Closes task #32's code side; the connected visual gate (shards gone at
the Bind Stone, base crystals and spin retained) is the acceptance.
App Release suite 3,968 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
General composite-translucency fix (16ed6e7c) user-verified on other items; the orb keeps traces. Remaining hypotheses (ClipMap-opaque shell / unattached-emitter scope split) and the discriminating probe set are recorded in the issue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user reported wielded items subtly hiding particle effects, as if a
translucent texture were missing. Root cause verified in source: the
DAT authors a per-surface Translucency float, and the shared-atlas
extraction honors it by baking (1 - Translucency) into the texture
alpha (MeshExtractor). But a surface with an appearance override -
ObjDesc subpalettes or texture changes, which wielded loot typically
carries - routes through the per-instance composite paths instead
(WbDrawDispatcher.ResolveTexture -> TextureCache
GetOrUploadWithPaletteOverrideBindless /
GetOrUploadWithOrigTextureOverrideBindless -> DecodeFromDats), and the
textured decode there never saw the authored value: only the
Base1Solid branch passed it (SurfaceDecoder.DecodeSolidColor);
DecodeRenderSurface has no translucency input at all.
Consequence: the part still classified translucent, still sorted in
the RetailAlphaQueue, still drew with depth writes off - but with
texture alpha = 1 it overwrote everything already composited behind
it. Particles behind the part vanished; particles in front survived.
The same GfxObj without overrides (atlas path) rendered correctly,
which is why the loss was so selective and subtle.
Fix: SurfaceDecoder.ApplyAuthoredTranslucency mirrors the atlas bake
(in-place alpha scale, caller-owned buffers, Magenta sentinel
guarded), and DecodeFromDats applies it behind an opt-in flag set by
exactly the two world composite paths. The sky path stays unbaked (its
shader applies the authored opacity separately - baking would
double-apply, the AP-89 compounding class) and particle sheets stay
unbaked (emitter-driven alpha, no authored-translucency consumer).
Composite cache keys already include the surface id, so the baked
alpha is cache-coherent.
This closes an unregistered divergence (no register row existed; the
fix restores parity with the shipped atlas mechanism, so none is
added). Investigation evidence: equipped children and world objects
share the same classification chain (ClassifyPackedBatches/GroupKey),
so the gap was override-driven, not attachment-driven - a dropped item
with the same ObjDesc was equally affected.
Core SurfaceDecoder tests 22/22 (3 new); App Release suite 3,968 / 3
skips. Visual gate: a wielded item with authored-translucent parts
must let its particle effects show through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dragbar port (e4c99f54) armed the press path but the combat/spell
bar still would not move in the live client, and the move cursor kept
showing with the UI locked. Two distinct causes, both reported from
the user's connected session:
1. Snap-back: the combat/spell bar mounts ANCHORED (Left|Bottom), and
ApplyAnchor runs every frame before drawing children, recomputing
Left/Top from margins captured at mount. The drag wrote Left/Top and
the very next layout pass wrote them back - the window never visibly
moved. (The unit harness runs no per-frame layout, which is why the
original tests passed; unanchored windows like inventory never hit
this.) Interactive window moves AND resizes now re-baseline the
anchor capture on every applied change, and
RetailWindowManager.MoveTo/ResizeTo get the same rebase so
programmatic moves of anchored windows cannot be silently undone
either. ResetAnchorCapture is exactly the documented tool for this
("make the current geometry the new layout baseline after an
intentional change").
2. Locked cursor: the cursor the user saw was never the window-move
feedback path (which is lock-gated) - it was the dragbar's own
authored MD_Data_Cursor, revealed the moment the element began
claiming the pointer. Authored cursor resolution now suppresses a
WindowMoveHandle element's cursor while the UI is locked, matching
the radar's existing locked behavior of hiding its authored drag
affordance; movement itself was already gated.
Two inversion-sensitive regression tests: an anchored window dragged by
its handle must hold its position ACROSS an ApplyAnchor pass, and the
authored handle cursor must disappear when UiLocked flips on. App
Release suite 3,968 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The combat bar and spell bar could not be moved at all: their window
mounts Draggable=false (correct - retail never whole-surface-drags
them) and the authored move mechanism was missing. Retail registers
element class 2 as UIElement_Dragbar (Register @ 0x0046C840); a press
inside it calls UIElement::StartMovement on its parent window
(StartMouseMoving @ 0x0046C760) and release calls StopMovement
(@ 0x0046C7C0). The combat/spell bar layout (LayoutDesc 0x21000073)
authors exactly one such element - a 600 x 5 strip along the top edge,
which is where the user expects the move cursor. The powerbar, vitals,
indicators, radar, and examination layouts author dragbars too, so
they all gain their retail handles from this one port.
Our importer knew Type 2 by name but built it as a generic
UiDatElement - ClickThrough decoration, so the strip never even
claimed the pointer. Now:
- UiElement.WindowMoveHandle marks an authored handle; the DAT factory
sets it for Type-2 elements and opts them out of ClickThrough.
- A left-press inside a handle subtree moves the handle's top-level
window (the outer frame directly under the root - the mounted
analogue of retail's dragbar parent) even when that window is not
whole-surface Draggable. Edge-resize still wins; UiLocked still
gates, matching the retail locked/fixed parent-flag check.
- HoverWindowMove reports the handle so the window-move cursor shows
over the strip - and only there - on non-Draggable windows.
Four new tests: handle press moves a non-Draggable window and stops on
release, hover shows the move cursor over the strip but not the body,
UiLocked suppresses both, and the factory builds Type 2 as a
pointer-claiming move handle. App Release suite 3,966 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the crash the user hit twice today (captured in
artifacts/coldeve-acceptance-20260729/crash-hunt.log): clicking into a
multiline UiField - the examination window's inscription field - after
the text had changed since the last draw threw an unhandled
ArgumentOutOfRangeException from String.Substring and took the whole
client down (UiField.MeasureRange <- HitChar <- OnEvent MouseDown).
Root cause: _wrappedLines is a DRAW-side cache (rebuilt only in
DrawMultiLine) consumed by the INPUT side (HitChar on MouseDown and
drag-select MouseMove). Input events are pumped before the frame's
draw, so a mutation (backspace, SetText, paste) followed by a click in
the same pumped frame handed HitChar wrap lines describing the OLD,
longer text; measuring those stale ranges ran past the end of the live
string.
Fix: text mutations now bump a version (the _text field became a
private property so every existing mutation site participates without
churn), the draw records which version its wrap lines describe, and
HitChar proves coherence via EnsureWrappedLinesCurrent() - rebuilding
with the last draw width when stale. Rebuilding rather than clamping
keeps caret placement CORRECT against the live text, not merely
non-throwing. Two inversion-sensitive regression tests reproduce the
exact crash sequence (wrap long text, shrink without a draw, click);
they throw without the HitChar coherence call.
App tests 3,962 passed / 3 skipped (3,960 + 2 new).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 4 made a remote character's wielded weapon selectable, which made the
pickup chain reachable end to end for the first time: SelectionPickUp on
another player's weapon captured identity, passed ValidatePickupTarget (which
checked only the Stuck flag and the small-item mask, and a MeleeWeapon clears
both), installed a real non-autonomous approach through
PlayerInteractionMovementSink, and then sent a pickup request the server
rejects. Retail does none of that.
ItemHolder::AttemptToPlaceInContainer @ 0x00588140 runs
AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0 first, at 0x00588173 --
ahead of container legality, auto-merge, the container walk, and the only
CM_Inventory::Event_PutItemInContainer emitter
(ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680). IsItemLegal's arm at
0x005872B7 rejects `!ACCWeenieObject::IsOwnedByPlayer(item) &&
item->pwd._location != 0` with one local
ECM_UI::SendNotice_DisplayStringInfo(0x1a, ...), and
CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 then withdraws the waiting slot it
had published (SetWaitingState(obj, 0) + SendNotice_EndPendingInPlayer at
0x0055D918). No request, no movement. acdream had never ported that arm; it
was harmless while wielded children were unpickable and stopped being harmless
at f6db964f.
The notice is data_7e2228, "The %s is being wielded by someone else!" -- WITH
the exclamation mark. IsItemLegal's six strings occupy one contiguous literal
block, 0x007e21f0 through 0x007e234c, one per arm in reverse code order, and
the two neighbours already ported here (0x007e227c "The %s cannot be picked
up!" at 0x00587264, 0x007e22b4 "You cannot pick up creatures!" at 0x005871f4)
pin it. The punctuation-free 0x007cd350 variant belongs to the wield/wear
block and is emitted from a different function at 0x00560aef.
pwd._location is the PublicWeenieDesc CurrentWieldedLocation field
(acclient.h:37175), which acdream projects as
ClientObject.CurrentlyEquippedLocation, and ACCWeenieObject::IsOwnedByPlayer
@ 0x0058D160 is IsOwnedByObject(this, player_id) -- already ported as
ClientObjectTable.IsOwnedByObject @ 0x0058CEB0 and reached here through the
existing ItemInteractionController.IsOwnedByPlayer. The arm reads pwd._location
verbatim rather than adding a WielderId belt-and-braces test, because retail's
predicate is the thing being ported.
The player's OWN wielded item is IsOwnedByPlayer, so retail passes it and takes
a different route. ACCWeenieObject::DeterminePositionState @ 0x0058BE70 gives
it PositionState.WIELDED (acclient.h:6802) rather than IN_3D_VIEW, and
UIAttemptPutInContainer records IR_PICK_UP only for IN_3D_VIEW, treating
WIELDED and IN_CONTAINER alike as a plain IR_PUT_IN_CONTAINER transfer. So an
own-wielded item is unwielded in place: the request goes out immediately with
no approach, joining the existing current-ground-object shortcut. The shortcut
carries an ownership conjunct so it can never outrun the 0x005872B7 gate.
TryGetApproach now refuses attached children outright, for the same
IN_3D_VIEW reason. An Attached projection's bookkeeping WorldEntity.Position
carries the PARENT's composed root (EquippedChildRenderController
.ApplyParentWorldPose), not the child frame CPhysicsObj::UpdateChild @
0x00512D50 composes, so an approach built from it walked toward the wielder.
Slice 4 de-parented the marker anchor but left this one parent-derived; no
approach can anchor on a wielder now.
The pick predicates are deliberately untouched. Picking, selecting, examining,
lighting-pulse identity, and the vivid-marker anchor on a remote's wielded
weapon all behave exactly as Slice 4 shipped them -- retail's sr_Select and
sr_Examine branches of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 never
consult IsItemLegal. The gate is the transaction, not the pick.
f6db964f's message asserted the slice introduced no deviation and owed no
retail-divergence-register row. That was wrong: the unported 0x005872B7 arm
was a deviation it made reachable. This commit ports the arm in full, matches
retail on the own-wielded path, and removes the parent-derived approach
anchor, so the record is corrected here and no register row is owed.
Gates: dotnet build green; AcDream.App.Tests 3,960 passed / 3 skipped;
complete Release solution 9,792 passed / 5 skipped;
tools\run-connected-world-lifecycle-gate.ps1 -SkipBuild RESULT=PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A click on a remote character's wielded weapon reported nothing. The picker
was already correct: RetailSelectionScene publishes every drawn part under its
own live-entity server GUID and RetailWorldPicker returns the weapon as the
polygon winner. The failure was downstream eligibility - WorldSelectionQuery
required TryGetInteractionEligibleRecord, whose _visible set admits
LiveEntityProjectionKind.World only, so the winning hit was discarded.
Retail has no such gate. Render::GfxObjUnderSelectionRay @ 0x0054C740
accumulates each hit under the drawn part's own physics-object id
(CPhysicsPart::get_physobj_id @ 0x0050D490), and CPhysicsPart::Draw @
0x0050D7A0 admits any drawn part whose physobj id is nonzero. An equipped item
is a first-class CPhysicsObj with its own id and part array
(CPhysicsObj::add_child @ 0x0050F870 via CSetup::GetHoldingLocation @
0x005213F0). There is no parent redirection and no wielded-specific rule, so a
click on a wielded weapon returns THE WEAPON'S GUID. PositionState.WIELDED is
distinct from IN_CONTAINER (acclient.h:6802), so container suppression never
hid a wielded selection either.
LiveEntityRuntime gains two scoped predicates: TryGetAttachedProjectedRecord
(a current Attached projection that is spatially projected) and
TryGetPickEligibleRecord (that arm plus today's World visible-set arm, with
the same WorldEntity.Id staleness recheck). TryGetInteractionEligibleRecord
and the _visible set are deliberately NOT widened - they feed radar,
auto-target, sticky/MoveTo establishment, and CombatAttackTargetSource, and
retail's radar has no wielded blips. A regression test asserts an attached
child stays out of that set while picking admits it.
Marker anchoring had the twin problem. SmartBox::GetObjectBoundingBox @
0x00452E20 pushes the picked object's OWN m_position - which for a child is
the frame CPhysicsObj::UpdateChild @ 0x00512D50 recomposes each tick as
Frame::combine(parent part frame, holding frame) - and
CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere by that
object's own part-array scale. acdream stores the PARENT's root in the child
projection's Position/Rotation because the child's MeshRefs are
parent-relative, which put the vivid brackets at the wielder's feet. The
composed child root is already published per frame to EntityEffectPoseRegistry
by EquippedChildRenderController.PublishChildPose, so selection now borrows it
through an injected Func<uint, Matrix4x4?> wired in LivePresentationComposition
beside the existing selection-sphere hook. There is no parent fallback: a child
with no published composed root has no live frame this tick and no sphere. Its
part-array scale comes from the spawn record, the same source
EquippedChildRenderController.TryRealize reads, because an Attached WorldEntity
carries the parent-derived pose rather than its own ObjScale.
The sr_Use branch of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 guards
ItemHolder::UseObject with `found->pwd._wielderID != SmartBox::player_id` at
0x004E5BE9 while still selecting and flashing. Equipped-child picking makes
that click reachable, so the gate ships with it as
IWorldSelectionQuery.IsWieldedByPlayer.
CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive, so the pulse lights the
clicked object's own part array only - clicking a weapon never flashes its
wielder. That follows from routing the pulse identity through the same
predicate.
RetailWorldPicker, RetailSelectionScene, WbDrawDispatcher, and
EquippedChildRenderController are untouched, as are all wire and physics paths.
The slice REMOVES an undocumented deviation (Attached projections excluded
from pick eligibility versus retail's part-id pick) and introduces none, so no
retail-divergence-register row is owed in either direction.
Gates: dotnet build green; AcDream.App.Tests 3,951 passed / 3 skipped;
complete Release solution 9,783 passed / 5 skipped;
tools\run-connected-world-lifecycle-gate.ps1 RESULT=PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The named-retail oracle settles the child-vs-parent question: retail's pick records part->physobj->id (CPhysicsPart::Draw 0x0050D7A0, GfxObjUnderSelectionRay 0x0054C740), equipped children are first-class CPhysicsObjs whose m_position IS the composed hold frame (add_child 0x0050F870, UpdateChild 0x00512D50), so a click on a wielded weapon returns the weapon's own guid with no parent redirection and no wielded-specific gate. Selection, the non-recursive click flash (SetLighting 0x00511A80), and the vivid brackets all anchor to the picked child; only sr_Use on your OWN wielded item is suppressed (0x004E5BE9).
The gap analysis found acdream's picker already correct - equipped children publish selection parts under their own guid and already win the ray test. The failure is downstream eligibility: PickAt requires the World-kind-only interaction set, so the winning hit is discarded. The slice is therefore a scoped pick-eligibility predicate plus a marker anchor sourced from the already-published child root pose - deliberately NOT widening the interaction/radar/auto-target set, which retail also keeps free of wielded items.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The acceptance session on Coldeve ran 20 portal transits with zero wedges and captured a real wire-loss recovery live (resend/s=1 nak-in=1 mid-session, converged net-final ledger, graceful logout) - the event class that permanently killed sessions before N1. #260 is closed on that evidence. The one unrelated observation (first-login run-on-the-spot until a recall reset, self-healed, not reproduced on relogin) is filed as #262 with hypotheses and the no-workaround rule restated. Campaign doc, roadmap, and CLAUDE.md pointers flipped to the closed record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All seven slices shipped and reviewed. The campaign doc status header and ISSUES.md #260 now record the implementation-complete state with every slice SHA; the campaign doc's double-encoded punctuation (one early PS5.1 ANSI round-trip) is repaired to clean UTF-8. Remaining acceptance: the user Coldeve endurance session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The final slice review verified every retail address claim down to the
three distinct gate strictness masks (0x41 strict for NAK/handshake, no-ZF
>= for the 5 s sweep) and found no handshake, eviction, or ring defect.
This acceptance settles the campaign's remaining bookkeeping debt the
review surfaced: TS-58 (no TimeSync/Echo keepalive), TS-59 (no Flow
report), TS-60 (no 140 s dead-link/referral), TS-61 (send-failure burns
sequence+key), and AP-126 (one monotonic clock) are now real register
rows instead of dangling citations in shipped code. DropAll additionally
resets the completed-sequence ring (INFO-4's latent session-reset trap),
and the ledger corrects the post-acceptance retry-drop attribution to
NetworkManager's pre-route (INFO-5). N6 SHA f9c5e47e and its revert line
recorded. Core.Net 757/757 green after the ring-reset change.
Campaign N's implementation is complete: N0-N6 all shipped, all reviewed.
The remaining acceptance is the user Coldeve endurance session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign N Slice N6, the final implementation slice.
ConnectResponse handshake retransmit:
- While the connection is unconfirmed, the Connect character-list pump
resends the IDENTICAL cleartext ConnectResponse (same sequence 1, same
cookie, the one encoded datagram - no new outbound state) on retail's
strict 0.333333333 s gate. Retail: ClientNet::ProcessConnection
@ 0x00545450, case cs_ConnectionRequestAcked @ 0x0054547B (the constant
load at 0x00545481; the mask-0x41 strictly-greater x87 test at
0x0054548C); ClientNet::SendConnectAck @ 0x005440F0 re-stamps
lastSentHandshake_ (0x00544102) and rebuilds the same cookie packet.
- Confirmation = the first checksum-valid post-negotiation packet whose
header lacks the ConnectRequest flag: retail's cs_ConnectionRequestAcked
-> cs_Connected edge (ClientNet::ProcessPacket @ 0x00545100, the 0x40000
exclusion at 0x0054514E, SetConnectionState(..., 5) at 0x00545160).
- The cadence rides the TransportClock (virtual-clock testable through
TransportClockSource); the Connect deadline stays wall-clock.
- ACE safety pinned against the N0 model: a duplicate while still
AuthConnectResponse re-routes idempotently through NetworkManager's
pre-route; after acceptance CheckState clause 2 drops it pre-CRC at
zero keystream cost.
- Pre-N6, one lost ConnectResponse was a hang to the Connect deadline;
the N5 decorator deliberately arms after this window, so nothing
covered it.
FragmentAssembler eviction (divergence register row AD-52):
- Partials evict 60 s after their last ACCEPTED fragment; the stamp
refreshes on every new fragment (retail's re-stamp rule,
ArrivedEphInfo::UpdateNetBlobID @ 0x0054AE00), so a merely-slow partial
can never age out - 60 s is a floor, not a tunable. Swept from
ReliableTransport.Sweep on retail's 5 s flush cadence
(Indicator::FlushTimedOutEphInfo @ 0x0054A3D0, the gate at 0x0054A3DC;
per-entry ArrivedEphInfo::fTimedOut @ 0x0054AE30). N4's RejectRetransmit
abandonment made an unrecoverable partial a REACHABLE permanent state;
the TTL reclaims it.
- A 64-entry completed-sequence ring drops late duplicate fragments of
already-completed messages instead of allocating a fresh partial that
can never complete (the completed-then-duplicate leak).
Fold-ins:
- N5 review LOW-5: NetProbeTests + LossyTransportDecoratorTests (the
static NetDiagnostics / Console.SetOut mutators) share one
DisableParallelization xunit collection so they never run alongside
classes constructing WorldSession.
- Campaign section 9: N6 ledger row recorded; N5 row verified carrying
4e290f00.
Gates: 757 Core.Net Release tests green (10 new); full solution Release
green (0 failures / 5 skips); connected lifecycle gate PASS; the
N5-strengthened connected loss gate PASS on its first live run (2%/seed 1:
dropped out=3 in=10, resends=1 nak-in=1 nak-out=5, cksum-fail=0
sanity-drop=0 uncached-nak=0).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The review verified all three FAIL conditions absent (zero DROP_PCT=0
cost proven from code AND the decorator-absent baseline logs; the gate
fails explicitly on zero drops and zero recovery; teardown ordering
intact and ACE-safe) and reconciled the loss-ledger arithmetic packet by
packet. This acceptance folds in its two MEDIUM strengthenings: the
recovery assertion is now a per-direction conjunction (a one-direction
regression can no longer hide behind the other counter) and the three
keystream-health invariants (cksum-fail, sanity-drop, uncached-nak) are
asserted zero, turning the gate from "something recovered" into "loss
happened, both directions recovered, and the cipher ledger converged".
The unrecoverable-tail caveat now names the EnterWorldBody single-shot
alongside logoff/Disconnect and records ACE's gapped 1/s NAK trigger as
the mechanism. Script parse-validated; N6's gate run exercises it live.
N5 SHA 4e290f00 and its revert line recorded in the ledger.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-requested (2026-07-29): the plugin API must eventually support
VTank-class automation state machines written as acdream plugins. The
research decodes the full Virindi Tank surface from wiki archives and
primary source (the meta FSM''s complete condition/action vocabulary and
.met encodings, the expression language''s 67-function catalog, all ten
nav-point types with .nav wire payloads, VTClassic''s loot-rule type ids
and .utl format), derives the implied host API surface, and grounds it
against acdream: the K2 headless-bot triad is already the right
substrate, the VTank-like engine itself belongs in plugin-land, and the
milestone is a 5-step bridge/query/enchantment/transaction/nav sequence
where steps 2-4 ride on landed M3/M4 work. Filed in the post-Vulkan
intake as a C-bucket milestone candidate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign N Slice N5 (docs/plans/2026-07-29-network-transport-campaign.md
section 8 rung 3): the permanent removal of the loopback blindness that let
#260 ship. Local ACE never drops a datagram, so every historical connected
gate was structurally incapable of exercising the N1-N4 recovery machinery;
from this slice on, tools/run-connected-loss-gate.ps1 runs the standard
lifecycle route through deterministic seeded loss and passes only on proven
non-zero recovery.
Observability:
- [net-tick] gains resend/s nak-out/s nak-in/s rej-in/s dup-drop/s parked/s
reclaim/s cache= nakset= - TransportStats window deltas mirroring the
acks/s cumulative-delta pattern, plus the two instantaneous depths (the
unbounded-like-retail sent-packet cache watchdog and the inbound NAK set).
TransportStats gains RejectsReceived (inbound RejectRetransmit packets).
Counters increment unconditionally; every string is behind
NetDiagnostics.ProbeNet (Code Structure Rule 5).
- WorldSession.Dispose emits one cumulative [net-final] totals line so the
loss gate asserts exact counters instead of reconstructing them from
rounded per-second rates.
- LinkStatusSnapshot.PacketLossPercentage is deliberately NOT wired: filed
#261 - retail's CLinkStatusAverages formula
(LinkStatusHolder::GetPacketLossPercentage @ 0x00411370) must be located
first; inventing a ratio is forbidden.
N4-review F3 fold-in:
- Fresh reliable sends stamp Header.Iteration = the session iteration
through the same shared retail header build already cited for Time (N3)
and the N4 control packets: FlowQueue::TransmitNewPackets @ 0x00547A60,
the stack build at 0x00547A84/0x00547AA8. The control-header rule now
holds across all three send shapes (fresh reliable, ack, NAK). ACE reads
neither Time nor Iteration inbound (campaign section 3) - wire-safe, and
resends keep the stamp verbatim per the N1 rebuild rule.
Loss injection (Transport/LossyTransportDecorator):
- IWorldSessionTransport wrapper with deterministic seeded per-direction
loss. Config via NetDiagnostics typed env properties read once:
ACDREAM_NET_DROP_PCT (0 = off = default), ACDREAM_NET_DROP_SEED (default
1), ACDREAM_NET_DROP_DIR (out|in|both, default both).
- Arming gate: NOTHING drops in either direction until the decorator has
FORWARDED the first ENCRYPTED outbound datagram - parse-free check on
length > 20 with EncryptedChecksum set in the LE flags word at bytes
4..8. The cleartext handshake always survives and the arming datagram is
never a casualty; handshake-loss testing belongs to N6's ConnectResponse
0.333 s retransmit.
- Structurally absent at 0%: WrapIfConfigured returns the raw transport -
WorldSession's default factory is the only production seam and a normal
run never constructs the decorator.
Root-cause fix the gate immediately exposed:
- The logoff-confirmation wait in Dispose processed inbound datagrams but
never pumped the transport, so a lost S2C logoff confirmation was
gap-detected but its healing NAK never went out. Retail's pump
(Client::UseTime @ 0x00411C40 -> PacketController::UseTime @ 0x005410D0)
runs until LogOffServer; the wait now sweeps per processed datagram,
making the logoff wait the third covered blocking pump (after Tick and
the handshake loops). A lost C2S logoff REQUEST remains unrecoverable by
ACE design (arrival-driven NAK; a quiet client is never NAKed - campaign
section 3 row 1), recorded in the gate header.
Gates:
- tools/run-connected-loss-gate.ps1 (-DropPct 2 -Seed 1): PASS vs local
ACE - the first automated observation of packet loss in project history.
Decorator ledger: dropped out=3 in=10 of forwarded out=183 in=496.
[net-final] resends=2 nak-in=2 nak-out=6 rej-in=0 acks-out=114
acks-in=119 dup-drop=0 sanity-drop=0 cksum-fail=0 parked=9 reclaimed=0
uncached-nak=0 cache=1 nakset=0. Every injected loss healed: both
ACE-driven C2S resend recovery (nak-in=2 -> resends=2) and client-driven
S2C NAK recovery (parked=9 -> nak-out=6) fired on a real connected
route, all six checkpoints validated, graceful logout confirmed, ACE
recorded the transport Disconnect.
- tools/run-connected-world-lifecycle-gate.ps1 (decorator absent): PASS -
zero behavior change on the no-loss baseline; the gate now defensively
clears the drop env vars.
- Core.Net Release: 747/747 (737 + 10 N5: decorator determinism/direction/
arming/structural-absence/env parsing, the 5% seeded WorldSession lossy
lifecycle with zero message loss both ways + ACE Headroom 256, the
[net-tick] field pins, the Iteration stamps).
- Full solution Release: 9,763 passed / 5 skipped / 0 failed.
Test-fixture note: FakeAceTransport gains AutoAdvanceOnBlockingReceive so
virtual time can move during the blocking Connect()/EnterWorld() pumps -
with the clock frozen there, a dropped handshake-window datagram could
never be NAK-healed (a fixture artifact, not a transport property).
Campaign section 9 ledger row added (SHA recorded at N6 kickoff).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The N4 review confirmed the draw-order reclaim design (invariant attacked
from five angles, held) and NAK fidelity down to the decomp''s x87
comparison masks. This acceptance commit settles the two process debts it
found: AP-125 (standalone control packets vs retail''s CoalesceData
piggyback - the ACE-safety divergence that has shipped since N3''s ack and
N4''s NAK) now has its register row; the false rounding-bug justification
in AckNakScheduler (0.6 x 1e7 rounds UP under IEEE-754, truncation never
lost a tick) is rewritten as the defensive hardening it actually is; and
Admission.Process''s defaulted draw-ordinal is now ulong.MaxValue so an
accidental cleartext repark can never head a bubble-shift chain. Core.Net
737/737 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign N slice N4 completes the AckNakScheduler NAK branch and closes
the ACE cleartext-reject keystream hazard - the slice that makes S2C
loss actually RECOVER.
NAK emission (SharedNet::EnqueueNaks @ 0x00543BD0):
- One cleartext exact-flags RequestRetransmit per sweep behind the
STRICT 0.6 s gate on the ONE shared timestamp (the x87 0x41-mask test
at 0x00543C03 proceeds only on strictly-greater; the ack's gate stays
>=). Never an ack in a NAK sweep; a NAK delays the next ack by 2.0 s
and vice versa (landmine #7).
- Body = u32 count + ids ascending, capped at 114 (ReceiverData::GetNaks
@ 0x005490C0, cap 0x72; the m_cbData = 4*count+4 store at 0x00543C3E);
header Sequence borrowed from highestIDSent_ without incrementing;
cleartext or ACE ignores it (landmine #6, NetworkSession.cs:283-284) -
and a NAK never refreshes ACE's 60 s timeout.
- Control-header rule decided once for BOTH ack and NAK: Time = the
interval id, Iteration = the session iteration, matching retail's
shared header build (FlowQueue::TransmitNewPackets @ 0x00547A60, the
stack build at 0x00547A84). ACE reads neither field inbound.
- Gate ticks now round instead of truncate: 0.6 has no exact double
form, and truncation opened the strict gate exactly AT the boundary.
RejectRetransmit reclaim (divergence register AD-51, ACE adaptation):
- ACE's RejectRetransmit consumes a FRESH sequence, cleartext, with NO
keystream word, and is cached (ACE NetworkSession.cs:299-304,
:722-725, :743-748) - the one place ACE breaks retail's gap-walk
invariant that every missing id was word-bearing (retail cleartext
always borrows live sequences). Unhandled, the gap walk parks a word
for the reject's id and the inbound stream runs permanently one word
ahead - the N2 desync class reintroduced through the reject path.
- Fix: on a VALIDATED cleartext reject, InboundSequenceTracker removes
the mis-park, shifts every later-drawn parked word down one position
(per-word draw ordinals; ascending wrap-safe id <=> ascending draw
order), and pools the excess word, consumed lowest-draw-order-first
ahead of fresh ISAAC draws. Exact for any number of interleaved
rejects in ANY arrival order - a plain reclaim FIFO is not: a reject
arriving after a higher encrypted arrival crosses the parked chain,
and two out-of-order rejects pool their excess words out of draw
order (both orderings pinned by tests).
- Reject BODY ids keep N2's discard: word-bearing server-side,
consumed-in-place. The pool is provably empty against retail servers.
N3 advisories folded (all five): honest transitional-state wording (the
empty N3 NAK branch could silently disconnect a loopback session at
ACE's 60 s timeout, witness [net-tick] acks/s=0), the
ReceiverData::SharedInit @ 0x00548EF0 (from Init @ 0x00548FA0)
citation, the FlowQueue::Empty pump-order wording (TransmitNaks ->
TransmitAcks -> TransmitNewPackets with the interval increment LAST @
0x00548A9D; our clock-first Sweep is cosmetic vs ACE), the
Time/Iteration rule above, and the stale WorldSession budget-break
comment rewritten to the sweep reality.
Tests: 737 Core.Net green (14 new in NakEmissionTests + updated N3
pins): strict-gate boundary, shared timestamp both directions,
NAK-xor-ack exclusivity, full wire-shape + 114-cap pins, model-served
retransmission round trip, five tracker reclaim proofs, the 130 s
virtual prune -> fresh-sequence reject system test (victim abandoned,
later traffic decodes, pool drains to zero), 10 s long-loss survival
(NAKs on the gate cadence, zero acks, heal inside the window), and the
capstone soak: 2% seeded bidirectional loss x 10,000 messages -> zero
message loss both ways, ACE crypto headroom 256 at convergence, every
ledger drained (cache at the single watermark entry - retail's Flush
prunes STRICTLY below the ack). Full solution Release: 9,758 passed /
5 skipped. Connected world-lifecycle gate PASS
(logs/connected-world-gate-20260729-150238); canonical nine-stop soak
PASS (logs/connected-r6-soak-20260729-150856).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Campaign N slice N3. Retail never acks per packet: SharedNet::EnqueuePak
@ 0x00543B10 is the binary's only AckSequence (0x4000) construction site,
gated at >= 2.0 s on ReceiverData::timeStamp_ (@ +0x10), armed at
connection birth by ReceiverData::Init @ 0x00548EF0, and arbitrated
NAK-xor-ack per sweep by ClientNet::ProcessConnection @ 0x00545450
(m_SeqIDsWeNAKed non-empty -> EnqueueNaks, else EnqueuePak;
SharedNet::EnqueueNaks @ 0x00543BD0 shares the SAME timestamp -
campaign landmine #7).
- New Transport/AckNakScheduler: owns the one shared timestamp; a
non-empty NAK set suppresses the ack (N4 emits RequestRetransmit in
that branch; in N3 it emits nothing - a documented transitional state,
safe for exactly one slice on loopback), else ONE cleartext exact-flags
AckSequence carrying the tracker's HighestIdReceived, header sequence
borrowed from HighestIdSent without incrementing, 4-byte LE body.
Flags are an EQUALITY, never an OR (landmine #5 - ACE's dedup
exemption NetworkSession.cs:342-343 and watermark-skip :474-476 both
require the exact value).
- ReliableTransport.Sweep pump order per FlowQueue::Empty @ 0x00548A20:
interval clock, NAK/ack arbitration, pending resends, prune. The sweep
already runs in Tick and both handshake pump loops (landmine #8), so
cumulative acks flow during the character-list/enter-world floods at
ACE's own ~2 s cadence.
- WorldSession: the Phase 4.9 per-packet reflex ack in ProcessDatagram
and SendAck are DELETED; the [net-tick] acks/s probe now reads
Stats.AcksSent; new internal TransportClockSource seam drives the
2.0 s gate on virtual time in the conformance suite.
- N1 Fable-review advisory retired (Time-stamp fold-in): fresh reliable
sends now stamp Header.Time = the current interval id, matching retail
FlowQueue::TransmitNewPackets @ 0x00547A60 (header build at
0x00547A84); resends already re-stamped. ACE never reads inbound
Header.Time, so the wire stays compatible.
Tests: 723 Core.Net (7 new) - gate cadence + watermark-at-emission,
flags-equality pin + model acceptance at the reused sequence without a
watermark advance, NAK suppression and resume after the gap clears, a
50-packet CreateObject flood collapsing to ONE ack, the quiet-session
keepalive property across a 120 s virtual horizon (the reflex ack's
keepalive role, replaced and proven against ACE's 60 s TimeoutDeadline),
the Time fold-in, and a full FakeAceTransport lifecycle with zero
CRC/state/duplicate drops. Full solution Release: 9,744 passed /
5 skipped / 0 failed. Connected world-lifecycle gate PASS (capped +
uncapped-reconnect, graceful exits, 0 failures); canonical nine-stop
route PASS (0 failures).
Campaign section 9 N3 row updated (complete; SHA recorded at N4
kickoff).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Campaign N Slice N2 (docs/plans/2026-07-29-network-transport-campaign.md
S2.2) - the second fatal #260 fix: the inbound keystream now aligns to
SEQUENCE order instead of arrival order. One lost S2C datagram no longer
desyncs the inbound cipher permanently - the missing id's pre-drawn key
parks in the NAK set, later packets keep decoding, and the retransmission
decodes with the parked key.
New src/AcDream.Core.Net/Transport/InboundSequenceTracker.cs - retail's
ReceiverData inbound half, ported rule for rule:
- Sanity window: drop when seq is wrap-safe newer than
highestIDReceived_ + 0x7FFF (SharedNet::SeqIDSanityCheck @ 0x00543A20;
the boundary itself is accepted).
- Duplicate/late arrival (encrypted, at/below the watermark): NAK-set
hit -> decrypt with the PARKED pre-drawn key; miss -> silent drop at
ZERO keystream cost (SharedNet::ProcessNewSeqNum @ 0x00544690, the
AVL::Remove branch) - the dup-word-burn and double-dispatch bugs close
together.
- Gap walk (SharedNet::ProcessNewestSeqNum @ 0x00541930): one inbound
ISAAC word per missing id, drawn IN SEQUENCE ORDER BEFORE the arriving
packet's own key (landmine #4), parked beside the id
(ReceiverData::AddNakked @ 0x00549240, idempotent; id 0 skipped per
retail's `if (esi_1 != 0)`). Cleartext walks to seq+1 - the borrowed
id itself gets NAKed, so the real encrypted packet at that id can
still decode later.
- Verify-failure re-park: a sequenced encrypted checksum failure parks
the consumed key back beside its id so the retransmission decodes
(SharedNet::ProcessPacket @ 0x00544790 tail, AddNakked(seq, &key)).
- Inbound RejectRetransmit -> silent NAK-set abandonment; parked keys
discarded, alignment holds because the words were already drawn
(SharedNet::HandleEmptyAck @ 0x005448F0).
- NAK set = SortedDictionary<uint,uint> seq -> parked key; ascending
raw-uint enumeration matches retail's AVL walk for N4's <=114-id NAK
emission (ReceiverData::GetNaks @ 0x005490C0).
PacketCodec split (campaign S4, retail's own factoring - the key is an
optional in/out of ReceiverData::Decrypt): TryParseBorrowed is the pure
parse + checksum-summand computation with NO keystream access anywhere;
VerifyChecksum(header, headerHash, payloadHash, uint? key) compares the
additive cleartext form (null) or headerHash + (key ^ payloadHash).
TryDecodeBorrowed(datagram, IsaacRandom?) - the consume-before-compare
site that WAS the bug - is deleted; the owned TryDecode stays
(test-only). RejectRetransmit ids are now exposed on both decoders
(borrowed RejectRetransmitBytes/Count like the Request pair; owned
RejectRetransmits list); the bytes were always inside the hashed span,
so parse-hash coverage is unchanged.
WorldSession: ProcessDatagram head is now parse -> sequence-0 split
(cleartext seq-0 = handshake/control, verified additively and processed
as before; encrypted seq-0 dropped before any keystream access, like
retail's ProcessPacket) -> tracker.Admit -> VerifyChecksum with the
admission key -> failure re-park -> unchanged flag handling, N1
transport consumption, reflex ack, and fragment loop. The
RejectRetransmit flag routes to the tracker beside the N1 NAK/ack
consumption. The handshake Connect loop moved to parse +
cleartext-verify (no tracker exists before ISAAC seeding; the
ConnectRequest is cleartext seq 0). ReliableTransport now takes both
Isaacs and exposes Inbound; the session's _inboundIsaac field is
deleted. No production caller constructed the N1 ctor outside
WorldSession, so no compatibility shape was kept.
TransportStats gains InboundDupsDropped, InboundSanityDrops,
ChecksumFailures, KeysParked (unconditional, like the N1 counters).
Watermark init = 1 is an ACE adaptation, register row AD-50 (watermark
INIT only, not a mechanism change; AD-49 stays reserved for the campaign
S5 blob-layer deferral): retail zero-inits ReceiverData, but ACE never
emits S2C sequence 1 - PacketSequence starts unprimed at uint.MaxValue,
the cleartext ConnectRequest takes NextValue 0, and the first ENCRYPTED
flush re-primes CurrentValue to 1 so the first encrypted sequenced
packet is 2 (ACE NetworkSession.cs:716-717 resolving to
UIntSequence(startingValue: 1), Sequence/UIntSequence.cs:9-13,30-41).
A zero-init watermark would gap-walk the permanent id-1 hole: one
spurious NAK, the first pre-drawn word mis-assigned to id 1, and the
keystream off by one from the first encrypted packet onward. holtburger
seeds the same value (crates/holtburger-session/src/session/api.rs:30,
last_server_seq: 1), mirroring ACE's own C2S-side
lastReceivedPacketSequence = 1 (NetworkSession.cs:57). The N0 model's
dance is pinned by the clean-lifecycle conformance test: min encrypted
S2C sequence == 2, zero NAKs, zero spurious drops.
Tests (+14; Core.Net 702 -> 716): the decisive gap test (10,11,13,14 -
13 and 14 decode with fresh words while 12's key parks with
KeysParked=1/NakCount=1, the late 12 decodes with the parked key, 15
takes the next fresh word - impossible pre-N2), zero-cost duplicate
drop (shadow ISAAC position unchanged), re-park -> byte-identical
retransmission decode, the cleartext borrowed-id rule, cleartext at the
watermark (no NAK/key/watermark change), sanity boundary +0x7FFF
accepted / +0x8000 dropped wrap-safe, skip-id-0 across the 32-bit wrap
with ascending NAK enumeration, RejectRetransmit abandonment with
alignment held, warm zero-alloc Admit; plus four real-WorldSession
conformance runs against the N0 ACE double: clean lifecycle (zero NAKs
at every stage), S2C loss of one packet of a Count=2 fragment set
(later packets STILL decode - the N2 win; late byte-identical
redelivery completes the split message intact), duplicate delivery
dropped BEFORE dispatch, and the seq-0 tracker bypass.
N3/N4 handoff notes are recorded in the campaign S9 N2 row: the interim
per-packet reflex ack acks the arriving sequence even while a gap is
parked (ACE prunes the lost id from its S2C cache before N4 could NAK
it - message recovery needs N3's retail NAK-xor-ack sweep), and ACE's
RejectRetransmit consumes a fresh CLEARTEXT sequence with no keystream
word, an ACE-vs-retail wrinkle N4's design must resolve.
Gates: dotnet build green; AcDream.Core.Net.Tests 716/716;
full-solution Release 9,732 passed / 5 skipped / 0 failed; connected
world-lifecycle gate vs local ACE RESULT=PASS (zero failures, one
pre-existing expected world-edge landblock-miss warning); canonical
nine-stop connected route RESULT=PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Campaign N Slice N1 (docs/plans/2026-07-29-network-transport-campaign.md
S2.1) - the direct #260 fix: every sent reliable packet is now cached and
re-emitted, header-rebuilt, when ACE NAKs a client-sequence gap. One lost
C2S datagram no longer voids every subsequent action for the session's
lifetime.
New src/AcDream.Core.Net/Transport/:
- TransportClock: injectable monotonic source + retail's 0.5 s interval
counter (ClientFlowQueue::IncrementLocalInterval @ 0x00547F10, tail
`intervalID_ += elapsed`; the same function's ~3 s TimeSync/Echo cadence
stays deferred per TS-58).
- SequenceMath: wrap-safe IsNewer/Max (TimeStampUtils::lhs_newer
@ 0x00543890, reduced to the signed-difference form).
- SentPacketStore: FIFO of ArrayPool-rented wire buffers; Add asserts
optionalLength == 0 (NetPacket::RemoveDisposableOptionalHeaders
@ 0x00549510 pinned as a no-op under standalone-control); FlushOlderThan
pops strictly-older wrap-safe (SentPacketStore::AddSentPacket
@ 0x0054AB00, Flush @ 0x0054ACD0).
- OutboundFlowQueue: owns the outbound ISAAC, highestIDSent (starts 1,
pre-increment, wrap 0xFFFFFFFF->1, never 0), the fragment sequence, the
store, the wrap-safe sorted dedup pending-resend list
(FlowQueue::EnqueueAcks @ 0x005488E0), and the flushNum_ ack watermark.
Cache commit happens AFTER a successful send
(FlowQueue::TransmitNewPackets @ 0x00547A60, commit site 0x00547C85).
NAK ids[0] folds into the watermark as retail's implicit cumulative ack
(RecipientData::ProcessNaks @ 0x00547010). A resend rebuilds ONLY the
20-byte header: flags Retransmission|EncryptedChecksum (|BlobFragments
with fragments), Time = current interval id, Sequence/Id/Iteration/
DataSize verbatim, checksum = fresh header hash + stored sealed checksum
(FlowQueue::TransmitAcks @ 0x005485B0, DequeueAck @ 0x005472F0). The
original ISAAC key rides inside the sealed value - no new keystream word
is ever drawn (CryptoSystem::EncryptData @ 0x0065FF40 non-null-key
path; landmines #1/#2). Resend only on explicit NAK (landmine #3).
- ReliableTransport: composition + Sweep() (interval clock, resends,
prune). The AckNakScheduler joins in N3/N4; ack behavior is untouched
this slice.
- TransportStats: unconditional counters (ResendsSent,
NakRequestsReceived, UncachedNakIds, AcksConsumed) + CacheDepth.
PacketCodec.FinalizeInPlace gains an overload returning (isaacKeyUsed,
sealedChecksum) where sealedChecksum is the pre-header-hash value -
payloadHash cleartext, isaacKey ^ payloadHash encrypted (retail
NetPacket::checksum_). The old signature forwards; encode bytes are
unchanged. Decode is untouched.
WorldSession integration is minimal: the transport is constructed at
ISAAC-seeding time (first reliable packet keeps sequence 2 / fragment 1,
byte-identical to pre-N1); SendGameMessage delegates (probe fseq/pseq now
read the transport); SendAck's borrowed sequence reads HighestIdSent
(identical value, behavior EXACTLY as-is this slice); ProcessDatagram
consumes RequestRetransmit + AckSequence BEFORE the unchanged reflex ack;
the sweep runs at the end of Tick() after the budget break AND inside
both blocking handshake pump loops (Connect step 4, EnterWorld
ServerReady - landmine #8), gated on _transportNegotiated; Dispose
returns the rented cache buffers.
Bookkeeping: TS-57 filed in the divergence register (uncached NAK ids
dropped silently + counted instead of retail's RejectRetransmit - ACE
no-ops the reject and the standalone unsequenced form would trip ACE's
watermark hole); TS-27 narrowed to the inbound direction in the same
commit; the stale WorldSession class-doc gap list corrected.
N0 fold-ins from the re-review: AceSessionModel.ProcessFragment split
into ACE's two literal branches (existing-buffer checks Complete,
NetworkSession.cs:495-507; new-buffer constructs + adds + TryAdds WITHOUT
checking Complete, :509-518), and the zero-count-fragment test now pins
the parked dead buffer (PartialFragmentBufferCount 0 -> 1). e3958610
recorded in the campaign ledger's N0 row.
Tests: 15 new in Transport/OutboundReliableTransportTests.cs - store
FIFO/strict/wrap-safe flush with rent/return balance via a counting
pool, interval-clock start/advance/wrap, resend header shape (flags
exactly 3 or 7, Time = interval, verbatim fields, checksum identity,
bit-identical body), resend-consumes-no-ISAAC-word, uncached-NAK
counting, ids[0] watermark fold + strict prune, wrap-safe ack max,
conformance resend verifying under AceCryptoModel with the ORIGINAL
parked key (Headroom 256, zero orphans, ordering restored), an
end-to-end FakeAceTransport lossy run (10 game actions, C2S #5 dropped,
all 10 dispatched in order, exactly one resend, session alive), and
zero-alloc steady-state SendGameMessage.
Gates: dotnet build green; AcDream.Core.Net.Tests 702/702; full-solution
Release 9,723 passed / 5 skipped / 0 failed; connected world-lifecycle
gate vs local ACE RESULT=PASS (0 failures, both sessions exit 0; one
pre-existing expected world-edge landblock-miss warning).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses the N0 review findings against commit 7e9134b4. Test-only: no
production code changes.
F1 (blocking) - model Session.CheckState (Session.cs:93-110). A three-value
AceSessionState (AuthLoginRequest -> AuthConnectResponse -> AuthConnected)
advances on SendConnectRequest (AuthenticationHandler.cs:127, :232) and on the
accepted ConnectResponse (NetworkManager.cs:77). CheckState runs as the first
statement of Receive after TryParse - ahead of the ConnectResponse route and
ahead of VerifyCRC - so a LoginRequest out of state, a replayed
ConnectResponse, or any of AckSequence|TimeSync|EchoRequest|Flow during
AuthLoginRequest is dropped at zero keystream cost (ACE's PacketHeader.HasFlag
is ANY-of, PacketHeader.cs:70). New StateDropCount counter.
F2 - implement SendBundle faithfully (NetworkSession.cs:808-919). One
NetworkBundle per GameMessageGroup (NetworkBundle.cs:6-63), swapped out and
sent in ascending group order; the InvalidQueue bundle carries the ack /
TimeSync / EchoResponse optional headers. As many same-bundle fragments as fit
the 464-byte body budget now travel in ONE packet - one sequence, one keystream
word - and a message whose remaining data fills a packet splits across packets
with Count>1 fragments (:846-854, :874-888) via a port of ACE's server-side
MessageFragment (MessageFragment.cs:10-103). The old "one packet per message"
shortcut and its incorrect rationale are gone.
F3 - model the two-phase termination. Terminate arms PendingTermination with
the 2 s window (Session.cs:281-298, SessionTerminationDetails.cs:12); inbound
and outbound keep running through it (Session.cs:124-133), then the pump
completes the session work and releases the network resources
(NetworkManager.cs:366-369 -> Session.cs:300-334 -> NetworkSession.cs:958-974).
IsTerminated now means "termination armed"; IsReleased is the point of no
return.
F4 - port ACE's MessageBuffer exactly (MessageBuffer.cs:7-54): a List, not an
index-addressed array. An assembled stream under 4 bytes returns null and is
dropped WITHOUT advancing the fragment gate (:49-50 + NetworkSession.cs:504-506
removing the buffer either way), and a later fragment claiming a larger
Count/Index for the same sequence completes the message instead of throwing.
F5 - the C2S parse path now characterizes ACE: fragment parsing uses ACE's
complete validation (16 <= Size <= 464, ClientPacketFragment.cs:12-24) with no
Count==0 / Index>=Count rejection and with ReadBytes' short-read tolerance,
instead of inheriting acdream's stricter production layout check. The one
remaining strictness we inherit - the 1024-id cap on retransmit lists - is
documented as unreachable (ACE reads into a 1024-byte buffer, so a C2S datagram
can carry at most 250 ids).
F6 - class doc now states that C2S CRC verification reuses acdream's own
PacketHeaderOptional hashing, so the double is NOT an independent oracle on
optional-header wire layout, and names the two known asymmetries (ACE has no
inbound ConnectRequest parse; ACE hashes-but-does-not-advance on
LoginRequest / WorldLoginRequest / ConnectResponse).
F7 - hardened three weak tests: the NAK rate limit is probed at 0.9 s and at
exactly 1.0 s (both closed) before 1.1 s opens it; the session timeout is
probed at exactly 60 s after fixing the model's `>` to ACE's `>=`
(Session.cs:140); the cache prune pins that an entry exactly 120 s old survives
(:258 is strictly greater).
F9 - campaign doc section 9 ledger: N0 row marked complete.
Nine new tests; 687 Core.Net tests green in Release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign N slice N0 (docs/plans/2026-07-29-network-transport-campaign.md):
the referee that slices N1-N5 are graded against, test-project only, zero
production changes.
- VirtualClock: Stopwatch-shaped deterministic time source (fixed 100 ns
ticks) that N1 will inject behind the production TransportClock.
- AceCryptoModel: verbatim port of ACE CryptoSystem Search/ConsumeKey over
our IsaacRandom - 256-key window, parked-key set, Headroom/OrphanCount
diagnostics (CryptoSystem.cs:8-49 cited per method).
- AceSessionModel: transport-free ACE NetworkSession over raw datagrams,
every rule cited to NetworkSession.cs - CRC-before-everything silent
drop, cleartext-NAK early return (no timeout refresh, :283-308),
60 s timeout refresh (:329-331), exact-equality ack dedup exemption
(:342-347), desired+2 NAK trigger with 1 s limit (:351-363), >window
AbnormalSequenceReceived (:393-397), the :474-476 watermark hole,
ack-value cache prune (:663-673), fragment gate (:532-543), seq>=2
caching (:730), Retransmission-flag resends with the ORIGINAL IssacXor
(:675-686), RejectRetransmit, 2 s cleartext cumulative ack, 20 s
TimeSync, EchoResponse, 120 s cache prune (:251-262). ACE's raw
wrap-unsafe comparisons are modeled bug-for-bug, not fixed.
- LossyLink: deterministic drop/reorder/seeded-loss fault injector, pure
data structure.
- FakeAceTransport: IWorldSessionTransport binding a REAL WorldSession to
the model through the link, with the handshake scripted (ConnectRequest
reusing the negotiation fixture layout, CharacterList, ServerReady,
logoff confirmation) - genuine Connect/EnterWorld/Tick/Dispose with no
sockets.
- 19 new tests pin the double, including
CleartextNonAckAdvancesWatermark_TheAceHole (the self-induced wedge
behind scope rows TS-57/TS-58/AP-125), re-key = permanent orphan,
unrequested-resend window burn, the 115-id NAK cap boundary, and a
full no-socket session lifecycle with both ISAAC streams verified
aligned end-to-end.
Core.Net suite: 678 passed / 0 failed (659 existing + 19 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLAUDE.md''s read-first list and the roadmap header now carry Campaign N
(retail reliable-transport port) as the active campaign and Campaign V
as the closed record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #260 investigation ended in a full root cause: acdream cannot
survive a single lost UDP packet in either direction. Outbound: the
server''s RequestRetransmit lists are parsed and consumed nowhere, and
no sent-packet cache exists - one lost C2S datagram permanently stalls
ACE''s ordered stream (actions void, position updates void, new areas
never stream: the whole #260/#256 symptom set). Inbound: the ISAAC
keystream is burned in arrival order, so one lost S2C datagram
permanently desyncs the cipher. Loopback ACE never drops packets,
which is why every historical gate passed.
The campaign doc pins the port target from the named retail decomp
(SentPacketStore/FlowQueue resend with reused ISAAC keys, the inbound
pre-drawn-key NAK set, the 2.0s cumulative ack / 0.6s NAK shared-gate
sweep, constants), the ACE constraint table Coldeve enforces (the
256-key crypto window, the exactly-AckSequence watermark rule, the
cleartext-NAK requirement), the Transport/ class design, slices N0-N6
with per-slice gates and Fable/Opus review assignments, the landmine
list, and eight divergence-register rows for the pieces that are
unsafe against ACE''s watermark hole.
#260 updated to point here; its memory half is closed as benign
(mapped-pak page residency + designed cache ceilings - measured, not
a leak).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-07-29 Coldeve session 3 stuck the player in the portal tunnel
forever: generation 2 (Town Network, 0x00070156) published render but
composites/collision never became ready, and the reveal latch correctly
held the tunnel. The composite warmup queue in WbDrawDispatcher has
exactly two permanent-stall shapes - a GfxObj id that never resolves
(silent load failure, e.g. custom-server content absent from the baked
pak) or an upload budget that never reopens - and they are
indistinguishable from the reveal log alone.
ACDREAM_PROBE_REVEAL=1 (NetDiagnostics.ProbeReveal) now emits one
[composite-warmup] STALL line per second while warmup blocks a reveal:
pending count, queue depth, scan state, upload-budget gate, and the
first four pending GfxObj ids. Zero cost when off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first armed ACDREAM_PROBE_NET launch died at exit 4 one second into
the world: the [net-tick] line read _inboundQueue.Reader.Count, but the
queue is built with SingleReader=true and that channel implementation
throws NotSupportedException from Count. The net thread now increments
and the frame thread decrements a probe-owned Interlocked counter
instead; behavior with the probe off is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two-agent investigation refuted #260's as-filed hypotheses: every
UseWithTarget was acked (the J5.2 use gate never latched), and the LOH
leak is bounded sawtooth churn - the real climb is ~2.25 GB of native/
GPU memory (WS 3,261 vs managed 1,015 MiB at wedge). The wedge evidence
also showed why it could hide: the live combat toggle routes through the
generation-gated runtime command seam, and every rejection exit in that
chain (Disposed / StaleGeneration / !IsInWorld at Validate, plus the
operations slot reading IsInWorld=false when unbound) is COMPLETELY
silent - no log, no event.
ACDREAM_PROBE_NET=1 (NetDiagnostics owner, PhysicsDiagnostics pattern)
now arms three probe families, all zero-cost when off:
- [net-out] per reliable send at the SendGameMessage chokepoint: opcode,
GameAction type+sequence, fragment/packet sequence, managed thread id
(two tids would prove the cross-thread ISAAC-desync hypothesis alone),
and state; [net-out-EX] via an exception FILTER that logs without
catching, so propagation is unchanged.
- [net-tick] 1 Hz cadence from WorldSession.Tick: inbound/s, queue
depth, budget breaks, worst inter-tick gap (frame-stall witness),
out/s, acks/s.
- [cmd-gate] every silent runtime-command rejection with expected-vs-
view generation, lifecycle, and IsInWorld, plus the combat toggle
result (whose Inactive exit reads a DIFFERENT IsInWorld source).
One walked-portal repro session with this probe distinguishes all
remaining #260 wedge hypotheses. ISSUES.md #260 rewritten to the
corrected two-root framing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First solid reproduction of the Coldeve findings: after sustained walked portal-network use the client wedges - server-round-trip actions (portal use, combat toggle) produce zero outbound send while client-predicted movement still works, and the LOH climbs to 574 MB. Combat toggle x3 with no send is the smoking gun for the one-request-at-a-time use gate latching closed on an unacked UseWithTarget. gcdump captured in the broken state. Supersedes the framing of #256/#257 - both are likely facets. Investigation not started; forbidden workaround (gate timeout) called out explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repeat connected gate 3/3 rendered on both witnesses, world-lifecycle route PASS with its one documented expected warning, resource snapshots banked. Every gate the campaign defined has now been executed and passed on the shipped tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SoundId was not a subset of retail's table, the way its comment claimed.
It was an invention: 23 acdream-local names on acdream-local values, and
the values were wrong in the way that matters. FootstepDefault = 0x02 is
retail's Random. SwingSword = 0x10 is retail's Death2. Death = 0x60 is
retail's Explode. Anyone who reached for one of those names to compare
against a wire or dat value would have got a different sound.
Nothing referenced any of them by name -- grep for `SoundId.` across src
and tests returns nothing -- so this was a trap rather than a live defect,
the same shape the enum campaign found in DamageType. All 22 invented names
are deleted and retail's 205 replace them.
Three oracles agree exactly, on every name and every value: retail
acclient.h:4569 enum SoundType, ACE's Sound, and DatReaderWriter's Sound.
The third matters most. AudioHookSink already resolves SoundTable lookups
through DatReaderWriter.Enums.Sound, so that is the enum acdream actually
reads at runtime; our catalog now agrees with the values already flowing
through the dat path, and a conformance test pins the two so they cannot
drift apart.
On the "206 sounds" figure: retail's block holds 207 entries, being 205
sounds followed by NUM_SOUND_TYPES = 0xCD and FORCE_SoundType_32_BIT. The
first is a count and the second a width pin. Counting the former is where
206 came from. Neither is a member here, matching how the campaign treated
NUM_ATTACK_HEIGHTS and Num_HoldKeys -- a count is not a value the wire can
carry.
Behaviour is unchanged and could not be otherwise: the enum had no
consumers. IAudioEngine's three SoundId overloads are no-op stubs and the
live path takes wave ids and DatReaderWriter values.
The user's separate report that sound is "not working that good" is a
triggering, selection and attenuation question rather than a catalog one,
and is filed as its own Bucket B row in the post-Vulkan intake.
Also in this commit, by user decision: AC2D is retired as a reference. Its
clone and directory are gone and it must not be re-cloned. Everything we
took from it still stands and is written down -- the FSplitNESW terrain
split constants, the 0xF61C movement packet layout, the finding that a
client need not compute terrain Z itself -- so CLAUDE.md's reference list,
its hierarchy table, and the architecture doc's protocol row now point at
docs/research/2026-04-12-movement-deep-dive.md rather than erasing the
history. The reference count drops from six to five.
Core tests 3907 passed / 2 skipped.
Co-Authored-By: Claude Fable 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>