3023 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
e9686401bc |
docs(net): N3 accepted - Opus review PASS, SHA 0265cc42, advisories to N4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
19bfb8477d |
docs(net): N2 accepted - Fable review PASS, SHA 46d209d0 recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
66513b16db |
docs(net): N1 accepted - Fable review PASS, ledger SHA + advisories recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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).
|
||
|
|
e395861053 |
test(net): N0 fix-up - CheckState gate, bundle coalescing, two-phase terminate
Addresses the N0 review findings against commit
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
200f19ce47 |
test(app): put every strict-zero site on the probe (#250)
The first commit converted the four members the issue named and left the other sites alone, reasoning that none had been observed failing. A 20-run complete-solution baseline disproved that within minutes: run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal… run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint… Both new names are the same shape as the four — one warm call, then a thousand-iteration loop inside the measured window — and neither had been recorded anywhere. "Not observed failing" only ever meant "not yet observed", and leaving known-shape sites in place would have guaranteed the acceptance gate failed. Run 19 is the sharper lesson: the issue named `SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first commit converted a *different* test in that same file, so the actually-named member was still on the old shape. Matching by file was not matching by test. Every strict-zero site in the assembly is now on the probe — ten tests. Two came out stricter rather than merely steadier: `StaticRenderProjectionJournalTests` was measuring a synchronise whose journal does **not** coalesce. Repeating it grew the journal by 1,000 entries per call — 192,000 by the end of a probe run — so the steady state the test claimed to measure did not exist and the single-call window had been hiding it. Its step is now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside the measured window for the first time and asserts the journal ends empty. `RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It now counts its own dispatches and pins the callback count against that, so the assertion still proves the fast path ran the callback every time without being coupled to a loop bound that no longer exists. Left alone deliberately: the four sites asserting a tolerance rather than zero — `CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb this noise and none has flaked; changing a bound in either direction is a separate decision from fixing a measurement. Worth noting that `PortalProjectionTests`' ceiling exists explicitly to tolerate "a tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch", which is exactly what the probe removes, so it could probably be tightened to zero now — recorded in the issue rather than done here. Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1d73ce524c |
test(app): measure the warmed path, not the path being warmed (#250)
The zero-allocation family failed about one full-suite run in three, on unchanged trees, and had been dismissed as inherent noise in `GC.GetAllocatedBytesForCurrentThread` three separate times. It is not noise. Reading the four members side by side, they share one root: **the measured window was never the warmed path.** UiDatFontTests 1 warm call, then a 10,000-iteration loop inline RenderFrameProductTests 8 warm calls, then a 1,000-iteration loop inline OracleTests 1 warm call, 1 measured call ArchRenderSceneTests warms Apply(registrations), measures Apply(updates) Two mechanisms come out of that table. A test method is JIT-compiled at tier 0 like anything else, and a long-running loop in tier-0 code gets replaced mid-flight by on-stack replacement — which compiles on the thread running the loop, so its bookkeeping is charged to the window being measured. That is the first two. And `ArchRenderSceneTests` warmed one arm of a switch and measured the other, so the measured call was the first ever into `ApplyUpdate` and paid that arm's JIT, type loads and static initialisation inside the window; `RenderFrameProductTests` warmed 8 times, below the tier-0 call-counting threshold of 30, so promotion was still pending when measurement began. That also explains the signature nobody could account for. Alone, the process is quiet and the runtime has finished before the assertion arrives. Alongside eight other test assemblies, tier-0 compilation never stops, the call-counting delay is re-armed continually, and the work slides into the window. Clean in isolation, failing under load, on a tree that changed nothing. `ZeroAllocationProbe` invokes the step many times before measuring anything, then measures windows that run the same already-warmed loop over the same already-taken path. Each window is a batch of 32 invocations and it reports the minimum across 4 of them. Both halves are load-bearing: the minimum is what excludes a one-time cost, and the batch is what keeps the assertion as strong as the loops it replaces — minimising over *single* invocations would report zero for a path that allocates every tenth call, which is a real regression made invisible. I had written it that way first and the apparatus test caught it. **The bound is untouched: exactly zero, no tolerance, no retry, no assertion relaxed.** `ZeroAllocationProbeTests` proves the apparatus can still fail — a step allocating every call reads above zero and does throw, a first-invocation cost reads as zero, a cost every tenth call is caught, and the one stated limit (the batch must cover the period) is pinned as a test rather than left as prose. Without those, a later edit could quietly make the whole family unfailable. Twelve further sites in this assembly still use the hand-rolled shape. None has been observed failing, and each needs its own repeatability analysis — several mutate state or consume monotonic sequences — so they are listed in the issue for adoption when next touched rather than converted blind at scale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ce9445b270 |
fix(render): the near plane is col3, not col4 + col3 (#248)
`FrustumPlanes.FromViewProjection` extracted the near plane with the Gribb-Hartmann form written for OpenGL's `[-1,1]` clip-space z range. Every acdream projection comes from `Matrix4x4.CreatePerspectiveFieldOfView` or `CreateOrthographic`, whose range is `[0,1]`. Under `[-1,1]` the near plane is the locus of `clip.z = -clip.w`, which is `col4 + col3`; under `[0,1]` it is `clip.z = 0`, which is `col3` alone. Concretely, the mismatch put the effective near threshold at `-n·f/(2f-n)` — about 0.5 m where the retail chase camera asks for 1.0 m. That error only ever kept geometry the true frustum would have dropped, never the reverse, which is why it produced no visible defect and was filed instead of hot-fixed during Campaign V. It is still wrong, and it is the same mistake that *was* visible in `PortalProjection`, where it culled the cell behind a doorway the camera stood close to. The far plane is `col4 - col3` under both conventions and is untouched. A test pins it anyway, so that a future edit to this function cannot drift it while nobody is looking. The acceptance criterion asked for a unit test pinning the extracted near distance to the camera's near value, and that is what landed: a theory over four near/far pairs asserting the plane is unit-length, faces down -Z, and stands off the eye by exactly `nearDistance`, plus a kept/dropped pair straddling it. The test was checked against the old formula before commit and fails all four cases there — it measures the fix rather than merely accompanying it. The other half of the acceptance criterion — unchanged culling in the offline pixel gate and the connected route — could not be run: #259 has Win32 surface creation failing machine-wide, so no gate that needs a window is available tonight. Recorded as outstanding rather than assumed. Solution build 0 errors; `AcDream.Core.Tests` 3,898 passed / 2 skipped / 3,900. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22ae7944b6 |
merge(net): the wire-stack audit, and one reconciled #255
Brings `github/overnight/wire-audit` (` |
||
|
|
cd2f3feae2 |
merge(core): the enum verification campaign, onto the post-deletion tree
Brings `github/overnight/enums` (` |
||
|
|
d312bd2ff1 |
docs(render): Campaign V V11 status — deleted and statically green, runtime gates blocked
Records what V11 actually achieved and, more importantly, what it did not.
The deletion landed: 204 files, +1,870 / -27,607 lines across five commits.
Section 5.5.24 keeps the three findings that outlive the diff.
* Chorizite could NOT be dropped, and not for the reason section 6 predicted.
The risk register assumed the package survived only because the ManagedGL
types implemented IUniformBuffer from it. The audit found TextureFormat in
the IWorldTextureArray signature the VULKAN path implements, and
BoundingBox serialized into the pak format. Dropping it is a slice that
touches the on-disk format, not a V11 cleanup.
* Two traps the V11 row did not know about. Studio/SampleData.cs is
production code behind the character sheet's fallback, so it moved rather
than died; ACDREAM_DEVTOOLS also gates Vulkan debug-utils, so the flag
survives and now says out loud that its UI is gone.
* Deleting GL surfaced a real bug: WbMeshAdapter.Dispose() was still
pattern-matching the GpuFrameFlightController that V6a replaced, so its
wait for submitted GPU work had been silently dead on every Vulkan run
since. Removing the type turned a no-op into a compile error.
The runtime gates did not run, and the honest reason is written down rather
than smoothed over. The client dies at vkGetPhysicalDeviceSurfaceCapabilitiesKHR
in files V11 never touched. Bisecting put the failure at the PRE-V11 commit
whose Vulkan soak had passed 91 checkpoints three hours earlier, and
`vulkaninfo --summary` -- a Khronos tool with none of our code -- fails at the
same call. Win32 surface creation is broken machine-wide; Vulkan itself is
fine. That is issue #259, with the one-line diagnosis at the top so the next
person checks the machine before bisecting the tree.
So the row reads DELETED AND STATICALLY GREEN, RUNTIME GATES BLOCKED. Release
build is 0/0 and the complete Release suite is 8,999 / 5 skipped (-218 against
V10, every one a test that lost its subject). Nothing was relaxed to
manufacture a pass: section 7.1 rule 2 cuts both ways, and a gate that could
not run is not a gate that passed. The rerun list is in 5.5.24, and the
pre-deletion pixel baseline was captured BEFORE the deletion, so the
self-differential is still available whenever a window can be made again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c265b52d4b |
docs(render): V11 closeout — register, architecture, code structure, issues
Retires the GL framing from the documents that described a two-backend,
two-UI-stack client, and files what the deletion left behind.
Divergence register:
* AD-46 (anisotropic tap pattern in dense alpha scenery) is REFRAMED rather
than retired. Its substance survives -- distant foliage may read denser
than retail's -- but it was measured GL-vs-Vulkan, and with GL gone it is
a Vulkan-vs-retail question against the D3D oracle it already cited. The
measurement is kept as the evidence that the residual is a driver tap
pattern; the row now records that it is no longer falsifiable by
self-differential, which is a real loss the deletion causes.
* AD-47 and AD-48 are NEW, and the campaign's own risk register scheduled
them here: MSAA sample positions (measured at 8.83% of the frame at 4x,
which is why every strict gate runs MSAA off -- and therefore why a
regression confined to the multisample path would not be caught) and
present pacing (#235 is the live instance).
* AD-17's justification moves from a GL clip-plane citation to Vulkan's
maxClipDistances floor, which is the same 8, so the divergence is
unchanged and only its authority moves.
* AP-92 keeps IUiViewportRenderer.TextureIsBottomUp rather than folding it
flat, because it is what let the origin question be answered by data.
Architecture and code structure: the layer diagram, the frame order, the
residency vocabulary and the reference table all said OpenGL. The UI section
said two stacks. Rule 3's rationale is rewritten around what actually
happened -- ImGui was deleted and not one panel, ViewModel or command had to
change, because none of them had ever imported ImGuiNET. That is the rule
paying for itself, so it is recorded as evidence rather than removed as
obsolete.
Issues: #258 files the dev-panel host as a decision rather than an accident,
and #255 is REOPENED. Its TaskCreationOptions.LongRunning fix asks the
scheduler for a thread but does not promise two callbacks overlap; under nine
concurrent test assemblies it still failed 2 of 5 whole-suite runs. The
earlier evidence tested a narrower pool, not a contended one. The fix it
needs is a rendezvous inside the read stub -- not a weakened assertion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5852bdb877 |
feat(render): Vulkan campaign V11 step 4 — retire GL from CI and gate scripts
Closes out the GL deletion by fixing the CI workflow and developer gate
scripts that still assumed a GL arm existed to compare against, build,
or select via ACDREAM_RENDER_BACKEND.
.github/workflows/headless-portability.yml: linux-graphical's "Verify
actionable unsupported-driver gate" step is deleted outright — it ran
the deleted `ui-studio` CLI verb (Studio was removed at Commit 1) to
prove the GL capability gate rejects Mesa's llvmpipe driver, and there
is no more GL capability gate for any driver to pass or fail. Its test
filter dropped two dead entries (GraphicalCapabilityRequirementsTests,
deleted at Commit 2; StudioWindowTests, already gone). Its package
contract check dropped the libcimgui.so assertion (ImGui's native
bridge, deleted at Commit 1). linux-vulkan's explanatory comment, which
described GL's rejection as the reason no cross-backend pixel diff runs
in CI, is rewritten to explain there is no GL arm left at all. Two dead
src/AcDream.UI.ImGui/** path triggers (that project no longer exists)
are removed from both the pull_request and push filters.
tools/run-backend-differential-gate.ps1 and its dedicated route file
tools/connected-backend-differential.route.txt are deleted: the whole
script's purpose was comparing a GL launch against a Vulkan launch of
the same route, and there is no second arm left to compare. Single-arm
regression checking already exists via run-offline-pixel-gate.ps1's
-Baseline mechanism.
tools/run-portal-churn-soak.ps1 is simplified rather than deleted: its
repeated-portal-churn methodology (within-arm capture comparison,
memory/entity/GPU trend analysis) has value independent of the
GL-versus-Vulkan question it was built to answer for issues #256/#257
before V11. -Backends now defaults to @('vulkan') alone; the doc
comments are rewritten from "step 0 discriminator, run before V11" to
an ongoing single-arm regression soak.
tools/run-offline-pixel-gate.ps1 drops its now-nonfunctional -Backend
parameter (ACDREAM_RENDER_BACKEND has read zero call sites since
RuntimeOptions.RenderBackend was removed at Commit 2 — passing -Backend
gl silently launched Vulkan anyway) along with its GL-escape-hatch
example and every comment that referenced the now-deleted differential
gate. tools/run-connected-world-lifecycle-gate.ps1,
tools/run-offline-vulkan-capture.ps1, and
tools/run-repeat-connected-gate.ps1 keep their (harmless, already
no-op) ACDREAM_RENDER_BACKEND set/clear lines but have their
now-inaccurate "escape hatch" / "GL run" comments corrected to state
plainly that the variable is unread and the line is kept only for the
historical record.
No .cs files touched; `dotnet build AcDream.slnx -c Release` unaffected
(0 warnings, 0 errors, matching the prior commit's build).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
7a0227c12e |
feat(render): Vulkan campaign V11 step 3 — drop the GL packages and shaders
Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8a7a0837e1 |
feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
41f74fcd2b |
docs: reopen #255, LongRunning is a hint and the test still flakes
The wire-stack audit's final full-suite gate turned up AnimationCache_CoalescesSameDidAndAllowsUnrelatedReadsInParallel failing twice in four runs on Windows, on a tree that already carries #255's LongRunning fix. Run alone the project passes 124 of 124 every time. Both failures were --no-build runs, which start faster and so crowd the machine harder, which is the original diagnosis rather than a new one. TaskCreationOptions.LongRunning asks for a dedicated thread. It does not promise one, and it does nothing about the other eight test projects saturating the box while these two forty-millisecond sleeps are supposed to overlap. The assertion on MaxConcurrentReads is therefore still measuring the host. The note suggests the shape that would actually close it: gate both threads on a barrier so each is provably inside the read before either is released. Then the count is a property of the loader instead of the scheduler and no amount of load can move it. Filed rather than fixed because this session's diff is confined to Core.Net, its tests, and docs, and the audit had no business editing content loaders on its way past. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
081c2009d1 |
docs(net): the wire-stack audit, all three hundred forty-nine opcodes
Bucket B item 2 of the post-Campaign-V intake, delivered as a ledger. Every entry in ACE's three opcode enums gets a row grounded in acdream source read during the audit, with a file:line citation, compared field by field against ACE's serializer or handler. The prior status columns were deliberately not used as input; they are compared against the result instead. Two blockers lead the document because they outlast it. The vendored reference repos are empty directories in the main checkout, not just in worktrees, so CLAUDE.md's instruction to cross-reference at least two of them is currently unexecutable as written; working copies were recovered elsewhere on disk and Chorizite.ACProtocol was not found anywhere. And the 2026-06-04 wire catalog this audit was meant to diff against was never committed and does not exist on disk, though MEMORY.md still links to it. The row universe therefore comes from ACE's enums, which is more complete than the missing catalog's 256, and drift is measured against the in-repo Phase-M matrix instead. The corpus could not support the frequency ranking the mission asked for. ACDREAM_DUMP_OPCODES was not set for any recorded session, so there are zero unhandled-opcode lines across every log. That is reported rather than papered over, and it is why no new parsers were written for unobserved gaps: the bar was observed plus two oracles, and the first half was unsatisfiable. One instrumented re-run would fix this, and it leads the open questions. The most useful thing the audit found is a pattern rather than a single bug. Three confirmed wire divergences each had a passing test built on the same wrong assumption as the parser it was defending. The Phase-M matrix had marked all three rows complete, and for ranged speech it even recorded the mistake as a design note, "same parser as 0x02BB". A status column can be wrong on the day it is written. Counts: 130 parsed or built-and-wired, 11 partial, 75 stub or dead-builder, 128 missing, 5 server-side, across 349 rows. The stub block is concentrated in fellowship, housing, trade, allegiance, and vendor, which is exactly the surface the intake's bucket-C items will need. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f57db35cec |
fix(net): xpSpent is a dword on the wire, and we were sending eight bytes
RaiseAttribute, RaiseVital, and RaiseSkill each wrote a 64-bit xpSpent, producing a 24-byte action where the server expects 20. ACE's GameActionRaiseAttribute and its Vital and Skill siblings read message.Payload.ReadUInt32(); holtburger's RaiseAttributeData declares xp_spent: u32 and advances the offset by four. Both oracles agree, and the four extra bytes were tail the server never reads. These three are live-wired, from the character sheet through the command router to SendRaiseAttribute, so this was shipping on every attribute, vital, and skill raise. It has not caused a visible failure because ACE reads the low dword and stops, and a single raise cost has never approached the dword ceiling. That is luck about value ranges, not correctness about layout. Worth noting the shape of the miss: the sibling builder BuildTrainSkill had already been corrected to a 20-byte, 32-bit credits field, and its test is even named U32CreditsNotU64. The same class of bug was found and fixed once in this file and the other three cases were left behind. The parameter stays ulong because the cost comes from 64-bit server XP tables several layers up in the App and Runtime command chain; narrowing that end to end is a separate change and is filed in the audit's open questions. Nothing is lost at the wire: a cost that does not fit in a dword was never expressible here. The existing test asserted the 24-byte shape and is corrected, joined by a theory that sweeps zero, one, a realistic cost, and uint.MaxValue across both remaining builders. Core.Net tests go 655 to 659. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f416c577d6 |
fix(net): stop dropping every transient string on a chat type that isn't sent
CommunicationTransientString (0x02EB) required a trailing u32 chat type after the message. The server does not send one. Because the string is padded to a four-byte boundary, the remaining length after reading it was always zero, the guard tripped, and the parser returned null for every transient string the server has ever sent. Not most. Every one. Three oracles agree there is no such field. ACE's GameEventCommunicationTransientString writes exactly one WriteString16L and stops. Retail's ClientCommunicationSystem::Handle_Communication__TransientString at 0x0057d460 takes a single PStringBase<char> argument. holtburger carries no type field for the event either. ParseTransient now returns the string. The wiring supplies chat type 0, which is ACE's ChatMessageType.Broadcast and which ACE's own LogTextTypeEnumMapper comment names "Default" — the honest stand-in for a message the server sends untyped. What retail's transient strings should actually look like is a rendering question and belongs with the chat colour work, not here. The existing round-trip test was itself appending the phantom trailing dword, which is exactly why the wrong guard looked correct for as long as it did. It is corrected to the real payload and joined by a case sweeping string lengths zero through four, so no future padding-residue assumption can hide here again. Core.Net tests go 654 to 655. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6119364306 |
test(net): pin the transport flag word against ACE, all twenty-three bits
The wire-stack audit checked the transport layer by hand and found it clean: PacketHeader's seven fields match ACE's Pack order exactly, the optional-header sections are parsed in ACE's order, and PacketHeaderFlags is a twenty-three of twenty-three value match including the sparse gaps between 0x04 and 0x100 and between 0x00800000 and 0x01000000. Clean is worth freezing. These bits are not design choices; each one gates an optional-header section, so a single wrong value shifts every following section's offset and takes the packet checksum with it. The failure would not look like a wrong flag, it would look like a corrupt connection. The enum is small enough to pin exhaustively, so this transcribes ACE's declaration and asserts both directions: every ACE flag exists here with ACE's value, and we declare nothing ACE does not. Core.Net tests go 630 to 654. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |