Commit graph

3038 commits

Author SHA1 Message Date
Erik
3a4782048e docs(research): Campaign P P2 response-layer edge family - AP-7 resolved, TS-1 mostly ported, #166 reattributed
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>
2026-07-30 07:54:32 +02:00
Erik
eed29a96f2 docs: Campaign P final visual-matrix runbook - 12 scenarios with setup/outcome/ledger mapping
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>
2026-07-30 00:15:51 +02:00
Erik
7a517ed02d docs: #262 triage round 3 - mode entry proven complete; [snap] apparatus found dead (DiagnosticLog never wired)
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>
2026-07-30 00:13:30 +02:00
Erik
96d6b58465 docs: #262 triage round 2 - (e1) stale blocks refuted by the #192 gate; three probe-discriminable candidates remain
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>
2026-07-30 00:11:15 +02:00
Erik
898d0f395b docs: #262 triage from the Coldeve acceptance log - hypothesis (a) demoted, stale-recenter survivors prime suspect (Campaign P P6)
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>
2026-07-30 00:09:49 +02:00
Erik
897b828dc1 docs: close #153 - far-teleport unstreamed-edge runaway severed at every causal link (Campaign P P5)
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>
2026-07-30 00:05:18 +02:00
Erik
9a0d1ae6f5 docs(research): #167 leash constants recovered from raw binary + arming flow (Campaign P P5)
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>
2026-07-30 00:01:17 +02:00
Erik
8cbe45f0b2 docs(research): Campaign P P3/P4 decomp anchors - remote residuals + world specials
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>
2026-07-29 23:58:40 +02:00
Erik
f6cbee59cc docs: close #72 - Humanoid turn omega settled by R6 DAT read + apply_run_to_command port (Campaign P P5)
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>
2026-07-29 23:37:40 +02:00
Erik
978ce1dcda docs: Campaign P - physics retail-feel parity plan (P1-P7 + final visual matrix)
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>
2026-07-29 23:32:42 +02:00
Erik
d1390bd84d docs: record Slice 4 equipped-child picking user acceptance (2026-07-29)
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>
2026-07-29 23:30:09 +02:00
Erik
e6a87679b7 fix(render): read TransparentPartHook opacity by the real part ordinal, not 0
Some checks failed
Headless portability / portable-headless (ubuntu-latest) (push) Has been cancelled
Headless portability / portable-headless (windows-latest) (push) Has been cancelled
Headless portability / linux-graphical (push) Has been cancelled
Headless portability / linux-vulkan (push) Has been cancelled
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>
2026-07-29 22:26:45 +02:00
Erik
bc05f0f61f docs: file #263 - Drudge Scrying Orb residual particle occlusion (deferred)
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>
2026-07-29 21:44:12 +02:00
Erik
16ed6e7c5c fix(render): keep authored surface translucency on composite textures
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The 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>
2026-07-29 21:40:26 +02:00
Erik
bfba0ecf7f fix(ui): interactive window moves must survive the per-frame anchor layout; lock the dragbar cursor
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The 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>
2026-07-29 21:24:49 +02:00
Erik
e4c99f54c0 feat(ui): port retail UIElement_Dragbar so authored drag strips move their windows
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The 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>
2026-07-29 21:04:33 +02:00
Erik
67379d1f9a fix(ui): UiField wrapped-line cache coherent with the text at mouse-hit time
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
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>
2026-07-29 19:38:52 +02:00
Erik
0ccbb4e52c fix(interaction): port retail's wielded-item pickup rejection (Slice 4 F1)
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>
2026-07-29 19:09:38 +02:00
Erik
f6db964fd5 feat(interaction): Slice 4 - equipped-child world picking
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>
2026-07-29 18:30:25 +02:00
Erik
9fdfe68c7f docs(interaction): Slice 4 spec - retail equipped-child picking research
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>
2026-07-29 18:10:05 +02:00
Erik
91d1d0d6f4 docs: Campaign N CLOSED - user-accepted; #260 closed; #262 filed
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>
2026-07-29 17:48:45 +02:00
Erik
5872826a13 docs(net): Campaign N implementation complete - closeout status, encoding repair
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>
2026-07-29 17:35:38 +02:00
Erik
27c5151189 docs(net): N6 accepted - Opus review PASS; five owed register rows filed
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>
2026-07-29 17:32:59 +02:00
Erik
f9c5e47e7f feat(net): N6 - ConnectResponse retransmit + fragment assembler eviction
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>
2026-07-29 17:20:12 +02:00
Erik
3899ebe0fd docs(net): N5 accepted - Opus review PASS; loss gate strengthened per review
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>
2026-07-29 16:40:55 +02:00
Erik
6077ce4d23 docs: VTank requirements research - the plugin-automation milestone model
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>
2026-07-29 16:29:49 +02:00
Erik
4e290f00d8 feat(net): N5 - loss observability, lossy decorator, the connected loss gate
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>
2026-07-29 16:26:06 +02:00
Erik
396838bb40 docs(net): N4 accepted - Opus review PASS; AP-125 filed; F1/F5 fixed
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>
2026-07-29 15:40:34 +02:00
Erik
852a59e388 feat(net): N4 - client NAK emission + RejectRetransmit reclaim
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>
2026-07-29 15:20:35 +02:00
Erik
e9686401bc docs(net): N3 accepted - Opus review PASS, SHA 0265cc42, advisories to N4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:03:33 +02:00
Erik
0265cc4236 feat(net): N3 - AckNakScheduler, retail 2.0s cumulative ack replaces per-packet acks
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>
2026-07-29 13:51:57 +02:00
Erik
19bfb8477d docs(net): N2 accepted - Fable review PASS, SHA 46d209d0 recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 13:11:54 +02:00
Erik
46d209d053 feat(net): N2 - inbound sequence-aligned ISAAC + NAK set
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>
2026-07-29 13:10:20 +02:00
Erik
66513b16db docs(net): N1 accepted - Fable review PASS, ledger SHA + advisories recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 12:25:57 +02:00
Erik
43e60a6971 feat(net): N1 - outbound sent-packet cache + resend on NAK
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>
2026-07-29 12:21:37 +02:00
Erik
e395861053 test(net): N0 fix-up - CheckState gate, bundle coalescing, two-phase terminate
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>
2026-07-29 11:42:36 +02:00
Erik
7e9134b4d1 test(net): N0 - ACE-behaviour double, virtual clock, lossy link
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>
2026-07-29 11:07:13 +02:00
Erik
9ed43e27df docs: point canonical state at Campaign N; record Campaign V closed
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>
2026-07-29 10:32:41 +02:00
Erik
b63d41b4e0 docs(net): Campaign N - the retail reliable-transport port
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>
2026-07-29 10:30:49 +02:00
Erik
b2b5e3d54a diag(render): composite-warmup stall probe for the session-3 tunnel hang
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>
2026-07-29 10:28:52 +02:00
Erik
d6a2e595c8 fix(diag): probe-owned inbound depth counter - SingleReader channels have no Count
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>
2026-07-29 08:56:17 +02:00
Erik
534bacbc23 diag(net): #260 outbound/command-gate probe + corrected issue framing
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>
2026-07-29 08:53:58 +02:00
Erik
ab3da28c34 docs: file #260 - portal-network wedge reproduced live (stuck action gate + LOH leak)
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>
2026-07-29 08:24:19 +02:00
Erik
50c0df0683 docs(render): Campaign V is closed - the deferred reruns pass on the GL-free tree
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>
2026-07-29 07:49:29 +02:00
Erik
39c1737bda feat(core): adopt retail's SoundType catalog; retire AC2D
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>
2026-07-29 07:38:56 +02:00
Erik
f27ad9ee43 feat(core): adopt retail's full WeenieError code table
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>
2026-07-29 07:33:27 +02:00
Erik
6530585309 docs(render): V11's pixel gate discharged - the deletion changed nothing
The WSI wedge cleared on its own and the armed tripwire caught the recovery. The offline pixel gate ran within minutes: post-deletion self-differential 13 px, and post-deletion versus the pre-deletion baseline also 13 px - removing 27,670 lines of OpenGL altered nothing about the Vulkan frame. Phase 1 of the overnight goal is complete as written; the connected-route reruns remain on the morning list as the goal's honesty hatch provided. #259 reclassified as transient and self-clearing, tripwire pattern recorded as the remedy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 06:20:25 +02:00
Erik
f433230940 docs(render): refine #259 diagnosis; ignore artifacts/ permanently
The #259 refinement (session-transition diagnosis, third gate attempt, cheapest-first morning remediation) as before - now without the 423 MB of session capture artifacts a git add -A accidentally swept into the previous tip commit. artifacts/ enters .gitignore so the mistake class is structurally impossible; the accidental commit is replaced via force-with-lease before anything consumed it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 04:03:27 +02:00
Erik
c503ed5aa8 docs(render): record the 03:56 re-attempt of the V11 pixel gate against #259
Tried the offline Vulkan self-differential once more at consolidated HEAD before conceding the night: the client still cannot create a window (same #259 signature - the fault vulkaninfo reproduces without our code). The shell is unelevated so a driver restart is unavailable, and a reboot would kill the session executing the goal. The negative is recorded with a timestamp so the morning rerun starts from evidence, not a re-bisect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 03:58:14 +02:00
Erik
b023ac95f0 docs: the overnight consolidation report, open questions first
Adds `docs/reports/2026-07-29-overnight-report.md` and brings the Campaign V
report page's status section in line with what is actually true.

The report leads with what needs the user, because most of it is genuinely
blocked rather than merely unfinished: the #259 reboot and the five V11 runtime
gates behind it, the enum campaign's three decisions (adopt `WeenieError`
wholesale? adopt retail's `SoundId` table or record our subset as a divergence?
re-clone the reference repos), the wire audit's one instrumented session, and
the walked-portal follow-up for #256/#257. Then what landed, with numbers, then
Campaign V's final state.

Two notes worth surfacing rather than burying. `references/` is empty **in the
main checkout**, not just in a worktree — which is why 456 of the enum
campaign's 864 property members are single-sourced, and why CLAUDE.md's
"cross-reference at least two of them" is currently unexecutable as written.
And two `MEMORY.md` entries index research documents that exist nowhere: not in
the tree, not under any ref, and the memory directory has no `research/` folder
at all. Two separate sessions hit that wall tonight and both regenerated from
scratch. User-level memory is out of my reach, so it is recorded as a
needs-user item with the two documents that supersede them.

The Campaign V page said V8 was running, V9 pending and V10/V11 ahead. All four
have landed. Its status section now records the deletion (204 files, +1,870 /
-27,607), the three findings worth keeping — Chorizite survives on the pak
format rather than on `IUniformBuffer`, `Studio/SampleData.cs` was production
code, and `WbMeshAdapter.Dispose()`'s GPU-work wait had been silently dead since
V6a — and, plainly, that the runtime gates did not run because this machine
cannot create a Win32 Vulkan surface for any process. A gate that could not run
is not a gate that passed. The #248 and #250 entries are updated to DONE with
their outstanding halves named.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 03:54:43 +02:00