From e77ebf100f5b252328a37697205e9c7cbe3fd6c0 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 15 Aug 2026 13:10:24 +0200 Subject: [PATCH] CC2 review fix round: latch scope narrowed, AD-100, creationFailed reason key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1 (MEDIUM): the correlation-latch docs claimed replies are never misattributed; in truth an overlapping send OVERWRITES the latch and the first reply routes to the newest request's event. Narrowed all three doc sites to the exact contract (single outstanding request; overlap refusal is CC3's Runtime verification gate, retail's DoFinish UNDEF-state rule) and pinned the overwrite behavior with OverlappingSend_OverwritesTheLatch_ReplyRoutesToNewestRequest. F2 (LOW): filed register AD-100 for the drop-unless-armed deviation — retail's Handle_CharGenVerificationResponse@0x0055E8B0 has no armed gate and processes whatever arrives against its persistent verification state. F3 (LOW): doc note in CharacterCreate.cs — ACE double-sends NameInUse (IsCharacterNameAvailable runs twice; the first callback's return exits only the lambda), so the second reply hitting the drop path during a connected gate is EXPECTED, not a defect. F4 (LOW): creationFailed's enum-member key renamed name -> reason and the ATTEMPTED character name added as name, before any consumer shipped — one status vocabulary must not give the same key two meanings (characterCreated.name is a character name). Contract, writer, tailer, and shape-pinning tests updated in lockstep. F5 (LOW): the thread-id probe-note pointer now cites ProbeNetLogOutbound's doc comment, where the note actually lives. Fidelity fold (reviewer's positive note): the latch is retail's OWN discriminator one layer down — 0x0055E8B0 case 1 branches on GetVerificationState()==PENDING (create) vs not (restore) — now cited in both the latch doc and CharGenVerificationResponse.cs. Core.Net 994, Runtime 1667, Launcher.Core 324, all green Release. Co-Authored-By: Claude Fable 5 --- .../retail-divergence-register.md | 3 +- docs/plans/2026-08-14-launcher-campaign.md | 15 ++++--- .../Messages/CharGenVerificationResponse.cs | 7 +++ .../Messages/CharacterCreate.cs | 14 ++++++ src/AcDream.Core.Net/WorldSession.cs | 45 ++++++++++++++----- .../Status/StatusEvent.cs | 10 ++++- .../Status/StatusEventParser.cs | 1 + .../Session/SessionStatusWriter.cs | 12 +++-- .../WorldSessionCharacterCreationTests.cs | 43 ++++++++++++++++++ .../Status/StatusEventParserTests.cs | 10 +++-- .../Status/StatusFileTailerTests.cs | 5 ++- .../Session/SessionStatusWriterTests.cs | 10 +++-- 12 files changed, 143 insertions(+), 32 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 8fb46bb4..f0da0f73 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -63,7 +63,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 75 active rows (AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) +## 2. Adaptation (AD) — 76 active rows (AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate visible-cell availability, full-catalog containment-root validation, and the @@ -192,6 +192,7 @@ readiness/requeue adaptation. See | AD-98 | **Filed 2026-08-15 at Campaign LA gate round 2 (character-select background tiling).** The LA8 root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=BottomEdge=0 ("no anchor") in the installed DAT, so retail's own `UIElement::UpdateForParentSizeChange` (0x00462640) never resizes this element — it stays a fixed 800x600 rect in retail's own widget tree. Retail's generic sprite blit, `Graphic::Draw` (0x00693b20) dispatching to `Graphic::PutImage` (0x00693a30) for an exact/undersized destination or a modulo-wrapped tile loop otherwise, has no third "stretch" mode (confirmed against `BlitMode`, acclient.h ~line 3135, and `MD_Data_Image::m_drawMode`/`DrawModeType` — both are COLOR-blend selectors, not tile-vs-stretch geometry modes). The only way retail's whole pre-world scene (background AND buttons AND listbox together) can still fill an arbitrary window resolution with no element ever resizing and a blitter that can only copy-or-tile is that these "flow" screens render into a fixed 800x600 target and the WHOLE FRAME is stretched once at presentation, outside the UI element/sprite system. **COMPLETED 2026-08-15 (same gate round, misalignment follow-up):** the first substitution (resize the mounted root + stretch only its own background) stretched the ART but left the authored child widgets at 800x600 pixel positions — misaligned against a background whose painting CARRIES visual anchors (the World/Characters captions are art). The substitution now reproduces retail's whole-frame behavior: the root KEEPS its authored 800x600 extent, and while the screen is active `UiRoot.FixedCanvasSize` scales EVERY emitted quad (widgets, glyphs, art, dialogs) uniformly at `TextRenderer.AppendQuad`, with the exact inverse applied to mouse coordinates at the `UiRoot` entry points so hit-testing lives in canvas space. Non-uniform window/canvas stretch, retail-authentic (no letterbox). `UiDatElement` keeps retail's pure copy-or-tile blit; the interim `StretchOwnBackgroundToFill` flag is deleted. | `src/AcDream.App/UI/UiRoot.cs` (`FixedCanvasSize`, `CanvasScale`, `MapWindowToCanvas`, `Draw`); `src/AcDream.App/Rendering/TextRenderer.cs` (`CanvasScale`, `AppendQuad`); `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (activate/deactivate/dispose set+clear the canvas) | Reproducing retail's literal mechanism (an offscreen fixed-resolution UI render target scaled at presentation) would add RHI surface area for an identical pixel result; scaling at the one quad-emission chokepoint with an inverse input mapping is the same math applied one stage earlier, and the world-space HUD stays native because the scale is scoped to `UiRoot.Draw`. | Glyphs stretch with the frame (retail-authentic blur at large windows). **Gate round 2 filtering follow-up (2026-08-15):** the stretch now filters bilinearly — `TextureCache.GetOrCreateLinearUiTwin` gives every nearest-sampled UI texture (dat-font glyphs, composited icons) a linear-sampled twin that `TextRenderer.DrawSprite` swaps to while `CanvasScale != One` — matching retail's own bilinear-filtered presentation blit instead of aliasing the point-sampled art. Any future fixed-canvas screen (login/disconnected/datapatch) sets `UiRoot.FixedCanvasSize` while active — per-screen opt-in, not automatic. If a genuine present-time frame-stretch pass ever lands, this collapses into it. | `Graphic::Draw` 0x00693b20; `Graphic::PutImage` 0x00693a30; `UIElement::UpdateForParentSizeChange` 0x00462640; `BlitMode` acclient.h ~3135; `UIElementManager::CreateRootElement` 0x0045d020; `CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf`; `UiRootFixedCanvasTests`; `UiDatElementTests.CanvasScale_StretchesQuadGeometry_LeavesUvsAuthored`; the NON-UNIFORM (no-letterbox) aspect behaviour has no decomp citation of its own (batch review F7) — it is inferred from the mechanism chain and CONFIRMED by the user's live gate pass 2026-08-15 (stretched widescreen look accepted as matching retail memory) | | AD-97 | **Filed 2026-08-14 at Campaign LA slice LA7a (character-restore request tail).** Retail's `CharacterRestore` request (`0xF7D9`) is ≥16 bytes: `CPlayerSystem::RestoreCharacter @0x0055d760` is, in the PDB-paired binary, `push 0x008173B4; push 0x008173B4; push guid; call Proto_UI::SendAdminRestoreCharacter @0x00546cf0`, and the callee packs BOTH constant `PStringBase*` arguments (`PStringBase::Pack @0x004fc6f0` emits ≥4 bytes even empty). Binary Ninja renders the two pushes as an uninitialized `edx` local plus `this` — a rendering artifact around constant `0x008173B4` (all 3 of its other pseudo-C appearances sit in provably-broken decompiles), but the arguments are real. acdream sends the 8-byte guid-only form. What the two constant strings contain is unresolved (a live cdb `db poi(0x008173b4)` would settle it). | `src/AcDream.Core.Net/Messages/CharacterRestore.cs` (`BuildRequestBody`) | ACE reads only `ReadUInt32()` and ignores any tail (`CharacterHandler.cs:331-385`), and holtburger ships guid-only from a real client command path against ACE successfully — the tail is unread by every server we can test against, and packing two strings whose CONTENT we cannot verify would be a guess. | A byte-capture comparison against a real retail client differs from offset 8; a future server that validates the full retail shape would reject our 8-byte request. | `CPlayerSystem::RestoreCharacter @0x0055d760` (binary bytes, not the BN rendering); `Proto_UI::SendAdminRestoreCharacter @0x00546cf0`; `PStringBase::Pack @0x004fc6f0`; ACE `CharacterHandler.cs:331-385`; holtburger `character_selection.rs:79-82`; LA7a Opus review F1 (2026-08-14) | | AD-93 | **Filed 2026-08-13 at social gate round 2, item 5 (the refused-drop notice port).** Two narrow gaps in the `ServerSaysAttemptFailed @0x0058EAE0` port: (1) **latched-guid preference** — retail's 0x00A0 dispatcher (`@0x0055B342`) PREFERS `prevRequestObjectID` over the wire guid when picking the item to name; acdream's `InventoryTransactionState.OnMoveFailed` instead REQUIRES the wire guid to match the latch (unobservable against ACE, which always sends the request's own guid on 0x00A0, and it protects a stale latch from mislabeling an unrelated failure — acdream has no retail-style latch timeout). (2) **unlatched request kinds** — retail latches `IR_MOVE`/`IR_WIELD` too; acdream's kind enum has no Move/Wield rows because wields ride `AutoWieldController` outside the single-request gate, so a refused wield/3D-move shows only the generic `HandleFailureEvent` leg, never "The X can't be wielded/moved". | `src/AcDream.Core/Items/InventoryTransactionState.cs` (`OnMoveFailed`); `src/AcDream.Core/Chat/InventoryFailureMessages.cs` (`Compose`'s absent Move/Wield rows); `src/AcDream.App/UI/ItemInteractionController.cs` (`OnInventoryRequestFailed`) | The match requirement is the compensating guard for the missing latch timeout; adding Wield/Move kinds means routing those sends through the single-request gate they deliberately bypass today — a behavior change beyond this gate item. | Only observable against a server that sends 0x00A0 with a guid that differs from the request's item (ACE never does), or on a refused wield/move, which shows no "can't be wielded/moved" verb line where retail would show one. | `ACCWeenieObject::ServerSaysAttemptFailed @0x0058EAE0`; the 0x00A0 dispatcher `@0x0055B342`; `ACCWeenieObject::RecordRequest @0x0058C220`; `docs/research/2026-08-13-confirm-and-weenie-error-display.md` §2 | +| AD-100 | **Filed 2026-08-15 at the Campaign CC CC2 review, finding F2 (unrequested `0xF643` handling).** When a `0xF643` (`CharGenVerificationResponse`) arrives with NO outstanding create/restore request, acdream DROPS the message with a once-per-session stderr log. Retail has no such gate: `Handle_CharGenVerificationResponse @0x0055E8B0` processes whatever arrives, discriminating create-vs-restore by its OWN persistent verification state (case 1 branches on `GetVerificationState() == PENDING` → new `CharacterIdentity` + `AddIdentity`, else unpacks into the existing identity at `slot`) — an unsolicited reply would be applied against whatever that state happens to be. acdream's transport-level latch (`PendingCharGenVerificationRequest`) is the equivalent discriminator, but when it is `None` there is no state to apply the reply against, so the honest move is drop-and-log rather than guessing a family. | `src/AcDream.Core.Net/WorldSession.cs` (the `CharGenVerificationResponse.ResponseOpcode` arm in `ProcessDatagram`; `_loggedUnexpectedCharGenVerificationResponse`) | Processing an unsolicited reply requires retail's persistent chargen verification state, which lives in CC3's Runtime owner, not the transport. Until then a reply with no outstanding request is either a server bug or a latch-lifecycle bug on our side — surfacing it in the log beats silently misrouting it to an arbitrary event. Pinned by `WorldSessionCharacterCreationTests.ResponseWithNoOutstandingRequest_IsDroppedAndNeverMisattributed`. | A server that sends a spontaneous/duplicate `0xF643` (ACE can double-send NameInUse — see the CC2 review's F3 note) has its second copy dropped here, where retail would re-process it. If CC3's verification gate ever needs retail's re-process semantics, this drop must move behind that owner's state. | `Handle_CharGenVerificationResponse @0x0055E8B0`; `ACCharGenData::GetVerificationState`; CC2 review F2 (2026-08-15) | | AD-99 | **Filed 2026-08-15 at Campaign LA gate round 2 finding 1 (character-select Exit button).** On a confirmed Exit, acdream closes the client through the existing graceful window-close path (`d.Window.Close`, the same seam `GameplayInputCommandController`'s in-world Escape fallback already uses) instead of retail's real post-confirm behavior: `RecvNotice_CloseDialog`'s case-1 arm queues UI mode `0x10000009`, which `gmEpilogueUI::Register` claims — a brief epilogue/farewell screen — before the process actually terminates. The confirmation dialog itself (`MakeConfirmExitDialog`, its exact `ID_CharacterManagement_ConfirmExit` text, and the `m_confirmExitDialogContext != 0` re-entry guard) IS ported faithfully; only the post-confirm destination differs, the same shape as AD-74's Options-panel exit. | `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (`RequestExit`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`CharacterSelectionRuntimeBindings.RequestExit`); `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs` (`d.Window.Close` binding) | acdream has no `gmEpilogueUI` port (out of scope this round); reusing the ONE existing graceful-shutdown seam keeps `disconnected`/`exited` status events firing through `GameWindow.OnClosing` → `CompleteShutdown` rather than inventing a second shutdown path, per explicit direction for this finding. | A user confirming Exit sees the window close immediately instead of retail's brief epilogue screen; a future feature wanting to reproduce that screen (or an intermediate "logged off, returned to character select" state) has no seam yet — same gap class as AD-44. | `gmCharacterManagementUI::MakeConfirmExitDialog @0x004ed250`; `RecvNotice_CloseDialog @0x004ed760` case 1; `gmEpilogueUI::Register(0x10000009)` @0x0047a680; `gmCharacterManagementUI::OnAction @0x004ed410` (Escape key, unported — button-only this round) | --- diff --git a/docs/plans/2026-08-14-launcher-campaign.md b/docs/plans/2026-08-14-launcher-campaign.md index 19e08478..88a9ad47 100644 --- a/docs/plans/2026-08-14-launcher-campaign.md +++ b/docs/plans/2026-08-14-launcher-campaign.md @@ -170,7 +170,7 @@ line, writer opens `FileShare.Read`, tailer opens `enteredWorld{characterId,characterName}`, `pluginLoaded{plugin}`, `pluginFailed{plugin,error}`, `loginCommandFailed{commandIndex,command,error}`, -`characterCreated{guid,name}`, `creationFailed{code,name}`, +`characterCreated{guid,name}`, `creationFailed{code,reason,name}`, `disconnected{reason}`, `exited{code,reason}` — every line carries `"v":1`, `"e"`, `"t"` (ISO-8601 UTC), `"sessionId"`. `secondsGreyedOut` is a uint on BOTH @@ -189,11 +189,14 @@ that payload's own field names and to read distinctly from `enteredWorld` — a freshly created character is logged straight in by retail without a fresh `characterList` (see that type's doc comment), so `characterCreated` can precede an `enteredWorld` for the same character -rather than replacing it. `creationFailed{code,name}` fires on any non-Ok -reply: `code` is the raw wire `CharGenVerificationResponse.Code` value, -`name` is that code's enum member name (e.g. `"NameInUse"`) so a reader -gets a stable readable reason without hard-coding the numeric mapping -itself. +rather than replacing it. `creationFailed{code,reason,name}` fires on any +non-Ok reply: `code` is the raw wire `CharGenVerificationResponse.Code` +value, `reason` is that code's enum member name (e.g. `"NameInUse"`) so a +reader gets a stable readable reason without hard-coding the numeric +mapping itself, and `name` is the ATTEMPTED character name so a launcher +can render "the name Bob is taken". (CC2 review F4: the enum member +originally rode the `name` key, colliding in meaning with +`characterCreated.name`; renamed before any consumer shipped.) `loginCommandFailed.commandIndex` is the zero-based index in the configured `loginCommands` array. `command` is the exact configured line and `error` is diff --git a/src/AcDream.Core.Net/Messages/CharGenVerificationResponse.cs b/src/AcDream.Core.Net/Messages/CharGenVerificationResponse.cs index b506c2bd..11eab682 100644 --- a/src/AcDream.Core.Net/Messages/CharGenVerificationResponse.cs +++ b/src/AcDream.Core.Net/Messages/CharGenVerificationResponse.cs @@ -25,6 +25,13 @@ namespace AcDream.Core.Net.Messages; /// from "create response" by opcode or shape alone — WorldSession /// disambiguates by tracking which outbound request (restore vs. create) it /// is awaiting a reply to (see WorldSession's awaiting-request latch). +/// That latch is not merely a reasonable design — it is retail's OWN +/// mechanism: Handle_CharGenVerificationResponse@0x0055E8B0 case 1 +/// branches on the client's persistent chargen state, +/// GetVerificationState() == PENDING → new CharacterIdentity +/// + AddIdentity (a create it initiated), else → unpack into the +/// existing identity at slot (a restore). Same discriminator, one +/// layer down (CC2 review's fidelity note). /// /// /// diff --git a/src/AcDream.Core.Net/Messages/CharacterCreate.cs b/src/AcDream.Core.Net/Messages/CharacterCreate.cs index ca008060..56aecaeb 100644 --- a/src/AcDream.Core.Net/Messages/CharacterCreate.cs +++ b/src/AcDream.Core.Net/Messages/CharacterCreate.cs @@ -123,6 +123,20 @@ namespace AcDream.Core.Net.Messages; /// warns about for restore. WorldSession's awaiting-request latch /// must never assume a reply is coming. /// +/// +/// +/// ACE double-sends NameInUse (CC2 review F3). +/// CharacterHandler.CharacterCreateEx calls +/// IsCharacterNameAvailable TWICE — once at the top and once after +/// PlayerFactory.Create — and the first callback's return +/// exits only the lambda, so a duplicate name yields TWO 0xF643 +/// NameInUse replies. The first consumes the latch; the second hits +/// WorldSession's unrequested-response drop path (register AD-100) +/// and logs "unexpected CharacterGenerationVerificationResponse". During a +/// connected gate against ACE that log line is EXPECTED after a +/// duplicate-name rejection, not an acdream defect — and CC3's verification +/// gate must not treat the second reply as an error. +/// /// public static class CharacterCreate { diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 9d2397b9..0881b2cb 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -723,16 +723,35 @@ public sealed class WorldSession : IDisposable /// create requests share that opcode on the wire (see /// 's doc comment) with no /// self-describing discriminant, so this latch is the only thing that - /// tells the dispatcher which event to fire. Set by + /// tells the dispatcher which event to fire. Retail's own discriminator + /// is structurally the same latch: Handle_CharGenVerificationResponse + /// @0x0055E8B0 case 1 branches on + /// GetVerificationState() == PENDING → new CharacterIdentity + + /// AddIdentity (create) versus not-pending → unpack into the existing + /// identity at slot (restore). Set by /// / /// immediately before the send; cleared the moment a matching 0xF643 is /// dispatched (success OR parse failure — a malformed reply must not /// wedge the latch open forever) and on session teardown - /// (). Read/written only from the caller's frame - /// thread — the same single-threaded invariant every other per-session - /// field here (e.g. ) relies - /// on; is never invoked concurrently with - /// a send (see the class doc comment's thread-id probe note). + /// (). + /// + /// SCOPE, stated exactly (CC2 review F1): this latch correlates + /// the SINGLE outstanding request. It does NOT refuse overlapping + /// requests — a second send while one is outstanding OVERWRITES the + /// latch and the first request's reply is then delivered to the wrong + /// event. Refusing overlap is the CALLER's job, exactly as in retail: + /// gmCharGenMainUI::DoFinish@0x004e9170 only sends when the + /// verification state is UNDEF (CC3's Runtime verification gate owns + /// that rule here). The overwrite behavior is pinned by + /// WorldSessionCharacterCreationTests so CC3 cannot silently + /// regress against it. + /// + /// Read/written only from the caller's frame thread — the same + /// single-threaded invariant every other per-session field here (e.g. + /// ) relies on; + /// is never invoked concurrently with a + /// send (see 's doc comment — the + /// #260 thread-id probe note; CC2 review F5 corrected this pointer). /// private enum PendingCharGenVerificationRequest { @@ -2306,9 +2325,12 @@ public sealed class WorldSession : IDisposable /// /// Send retail CharacterRestore through the control queue. This is /// deliberately non-blocking because ACE silently drops unknown guids. - /// Arms the awaiting-request latch as Restore BEFORE the send so - /// a reply that arrives on a later Tick is never misattributed to a - /// different request (Campaign CC CC2). + /// Arms the awaiting-request latch as Restore BEFORE the send; + /// the latch correlates the SINGLE outstanding request — a second + /// create/restore sent while this one is outstanding overwrites it, and + /// refusing that overlap is the caller's job (CC3's verification gate). + /// See (Campaign CC + /// CC2). /// public void SendRestoreCharacter(uint characterId) { @@ -2325,7 +2347,10 @@ public sealed class WorldSession : IDisposable /// non-blocking, matching — ACE /// silently drops a request whose packed account name doesn't match the /// session's own account. Arms the awaiting-request latch as - /// Create BEFORE the send (Campaign CC CC2). + /// Create BEFORE the send; the latch correlates the SINGLE + /// outstanding request — overlap refusal is the caller's job (CC3's + /// verification gate; see + /// ) (Campaign CC CC2). /// public void SendCharacterCreation( string accountName, diff --git a/src/AcDream.Launcher.Core/Status/StatusEvent.cs b/src/AcDream.Launcher.Core/Status/StatusEvent.cs index 2274baa8..6b63bfbe 100644 --- a/src/AcDream.Launcher.Core/Status/StatusEvent.cs +++ b/src/AcDream.Launcher.Core/Status/StatusEvent.cs @@ -64,13 +64,19 @@ public sealed record CharacterCreatedStatusEvent : StatusEvent /// /// Campaign CC CC2: a non-Ok reply to an outbound CharacterCreate. /// is the raw wire -/// CharGenVerificationResponse.Code value; is that -/// code's enum member name (e.g. "NameInUse"). +/// CharGenVerificationResponse.Code value; is +/// that code's enum member name (e.g. "NameInUse"); +/// is the ATTEMPTED character name. The enum member +/// rode the name key until the CC2 review (F4) — same key, +/// different meaning than characterCreated.name — renamed before +/// any consumer shipped. /// public sealed record CreationFailedStatusEvent : StatusEvent { public required uint Code { get; init; } + public required string Reason { get; init; } + public required string Name { get; init; } } diff --git a/src/AcDream.Launcher.Core/Status/StatusEventParser.cs b/src/AcDream.Launcher.Core/Status/StatusEventParser.cs index 9967613d..845d647c 100644 --- a/src/AcDream.Launcher.Core/Status/StatusEventParser.cs +++ b/src/AcDream.Launcher.Core/Status/StatusEventParser.cs @@ -272,6 +272,7 @@ public static class StatusEventParser T = t, SessionId = sessionId, Code = RequireUInt32(root, "code"), + Reason = RequireString(root, "reason"), Name = RequireString(root, "name"), }; diff --git a/src/AcDream.Runtime/Session/SessionStatusWriter.cs b/src/AcDream.Runtime/Session/SessionStatusWriter.cs index e0ce20d0..e4c43e12 100644 --- a/src/AcDream.Runtime/Session/SessionStatusWriter.cs +++ b/src/AcDream.Runtime/Session/SessionStatusWriter.cs @@ -232,12 +232,17 @@ public sealed class SessionStatusWriter /// /// Campaign CC CC2: a non-Ok 0xF643 response to an outbound /// CharacterCreate. is the raw wire - /// CharGenVerificationResponse.Code value; + /// CharGenVerificationResponse.Code value; /// is that code's enum member name (e.g. "NameInUse") so a /// launcher can render a readable reason without hard-coding the - /// server's numeric-to-dialog mapping itself. + /// server's numeric-to-dialog mapping itself; + /// is the ATTEMPTED character name — the thing a launcher most wants to + /// show ("the name Bob is taken"). The key was name for the enum + /// member until the CC2 review (F4): characterCreated.name is a + /// character name, and one status vocabulary must not give the same key + /// two meanings. Renamed before any consumer shipped. /// - public void CreationFailed(string sessionId, uint code, string name) => + public void CreationFailed(string sessionId, uint code, string reason, string name) => Write(new { v = VocabularyVersion, @@ -245,6 +250,7 @@ public sealed class SessionStatusWriter t = Now(), sessionId, code, + reason, name, }); diff --git a/tests/AcDream.Core.Net.Tests/WorldSessionCharacterCreationTests.cs b/tests/AcDream.Core.Net.Tests/WorldSessionCharacterCreationTests.cs index 0fb4f7ef..ecf08b90 100644 --- a/tests/AcDream.Core.Net.Tests/WorldSessionCharacterCreationTests.cs +++ b/tests/AcDream.Core.Net.Tests/WorldSessionCharacterCreationTests.cs @@ -199,6 +199,49 @@ public sealed class WorldSessionCharacterCreationTests Assert.Empty(createEvents); } + /// + /// CC2 review F1: pins the latch's stated scope EXACTLY. The latch + /// correlates the single outstanding request and does NOT refuse + /// overlap — a second send while one is outstanding OVERWRITES it, so + /// the first request's reply is delivered to the second request's + /// event. Refusing overlap is the caller's job (CC3's Runtime + /// verification gate, mirroring retail's DoFinish UNDEF-state gate). + /// If CC3 (or anyone) changes this transport-level behavior, this test + /// must change WITH it, deliberately. + /// + [Fact] + public void OverlappingSend_OverwritesTheLatch_ReplyRoutesToNewestRequest() + { + using WorldSession session = CreateSession(); + session.GameMessageCapture = (_, _) => { }; + + session.SendRestoreCharacter(0x50000001u); + session.SendCharacterCreation( + "testaccount", + MakeCreateRequest(), + new uint[CharacterCreate.SkillAdvancementClassCount]); + Assert.Equal(PendingLatch.Create, ReadPendingLatch(session)); + + var restoreEvents = new List(); + var createEvents = new List(); + session.CharacterRestoreReceived += restoreEvents.Add; + session.CharacterCreateResponseReceived += createEvents.Add; + + // This reply is semantically the RESTORE's — but the overwritten + // latch routes it to the create event. That is the documented + // overwrite behavior, pinned here. + byte[] packet = BuildPacket( + BuildVerificationResponseBody( + (uint)CharGenVerificationResponse.Code.Ok, + 0x50000001u, + "Restored")); + InvokeProcessDatagram(session, packet); + + Assert.Empty(restoreEvents); + Assert.Single(createEvents); + Assert.Equal(PendingLatch.None, ReadPendingLatch(session)); + } + [Fact] public void ResponseWithNoOutstandingRequest_IsDroppedAndNeverMisattributed() { diff --git a/tests/AcDream.Launcher.Core.Tests/Status/StatusEventParserTests.cs b/tests/AcDream.Launcher.Core.Tests/Status/StatusEventParserTests.cs index 10904453..e27f21c3 100644 --- a/tests/AcDream.Launcher.Core.Tests/Status/StatusEventParserTests.cs +++ b/tests/AcDream.Launcher.Core.Tests/Status/StatusEventParserTests.cs @@ -88,16 +88,18 @@ public sealed class StatusEventParserTests var failed = Assert.IsType( StatusEventParser.Parse( - """{"v":1,"e":"creationFailed","t":"2026-08-15T12:00:01Z","sessionId":"s1","code":3,"name":"NameInUse"}""")); + """{"v":1,"e":"creationFailed","t":"2026-08-15T12:00:01Z","sessionId":"s1","code":3,"reason":"NameInUse","name":"Bob"}""")); Assert.Equal(3u, failed.Code); - Assert.Equal("NameInUse", failed.Name); + Assert.Equal("NameInUse", failed.Reason); + Assert.Equal("Bob", failed.Name); } [Theory] [InlineData("{\"v\":1,\"e\":\"characterCreated\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"name\":\"NewChar\"}")] [InlineData("{\"v\":1,\"e\":\"characterCreated\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"guid\":1342177296}")] - [InlineData("{\"v\":1,\"e\":\"creationFailed\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"name\":\"NameInUse\"}")] - [InlineData("{\"v\":1,\"e\":\"creationFailed\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"code\":3}")] + [InlineData("{\"v\":1,\"e\":\"creationFailed\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"reason\":\"NameInUse\",\"name\":\"Bob\"}")] + [InlineData("{\"v\":1,\"e\":\"creationFailed\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"code\":3,\"name\":\"Bob\"}")] + [InlineData("{\"v\":1,\"e\":\"creationFailed\",\"t\":\"2026-08-15T12:00:00Z\",\"sessionId\":\"s1\",\"code\":3,\"reason\":\"NameInUse\"}")] public void MalformedCharacterCreationEventsUseTheKnownEventFailurePath(string line) { var malformed = Assert.IsType(StatusEventParser.Parse(line)); diff --git a/tests/AcDream.Launcher.Core.Tests/Status/StatusFileTailerTests.cs b/tests/AcDream.Launcher.Core.Tests/Status/StatusFileTailerTests.cs index bf30d037..4f9314aa 100644 --- a/tests/AcDream.Launcher.Core.Tests/Status/StatusFileTailerTests.cs +++ b/tests/AcDream.Launcher.Core.Tests/Status/StatusFileTailerTests.cs @@ -195,7 +195,7 @@ public sealed class StatusFileTailerTests : IDisposable AppendShared( """{"v":1,"e":"characterCreated","t":"2026-08-15T12:00:00Z","sessionId":"s1","guid":1342177296,"name":"NewChar"}""" + "\n" - + """{"v":1,"e":"creationFailed","t":"2026-08-15T12:00:01Z","sessionId":"s1","code":3,"name":"NameInUse"}""" + + """{"v":1,"e":"creationFailed","t":"2026-08-15T12:00:01Z","sessionId":"s1","code":3,"reason":"NameInUse","name":"Bob"}""" + "\n"); var tailer = new StatusFileTailer(_path); @@ -207,7 +207,8 @@ public sealed class StatusFileTailerTests : IDisposable Assert.Equal("NewChar", created.Name); var failed = Assert.IsType(events[1]); Assert.Equal(3u, failed.Code); - Assert.Equal("NameInUse", failed.Name); + Assert.Equal("NameInUse", failed.Reason); + Assert.Equal("Bob", failed.Name); } [Fact] diff --git a/tests/AcDream.Runtime.Tests/Session/SessionStatusWriterTests.cs b/tests/AcDream.Runtime.Tests/Session/SessionStatusWriterTests.cs index d9b0892a..90848778 100644 --- a/tests/AcDream.Runtime.Tests/Session/SessionStatusWriterTests.cs +++ b/tests/AcDream.Runtime.Tests/Session/SessionStatusWriterTests.cs @@ -110,7 +110,7 @@ public sealed class SessionStatusWriterTests writer.PluginFailed("s1", "acdream.bad", "failed"); writer.LoginCommandFailed("s1", 0, "", "unknown command"); writer.CharacterCreated("s1", 0x50000001u, "NewChar"); - writer.CreationFailed("s1", 3u, "NameInUse"); + writer.CreationFailed("s1", 3u, "NameInUse", "Bob"); writer.Disconnected("s1", "stopped"); writer.Exited("s1", 0, "disposed"); @@ -132,7 +132,7 @@ public sealed class SessionStatusWriterTests var writer = new SessionStatusWriter(file.Path); writer.CharacterCreated("s1", 0x50000010u, "NewChar"); - writer.CreationFailed("s1", 3u, "NameInUse"); + writer.CreationFailed("s1", 3u, "NameInUse", "Bob"); string[] lines = File.ReadAllLines(file.Path); Assert.Equal(2, lines.Length); @@ -149,8 +149,10 @@ public sealed class SessionStatusWriterTests Assert.Equal("creationFailed", failed.GetProperty("e").GetString()); Assert.Equal("s1", failed.GetProperty("sessionId").GetString()); Assert.Equal(3u, failed.GetProperty("code").GetUInt32()); - Assert.Equal("NameInUse", failed.GetProperty("name").GetString()); - AssertExactProperties(lines[1], "v", "e", "t", "sessionId", "code", "name"); + Assert.Equal("NameInUse", failed.GetProperty("reason").GetString()); + Assert.Equal("Bob", failed.GetProperty("name").GetString()); + AssertExactProperties( + lines[1], "v", "e", "t", "sessionId", "code", "reason", "name"); } [Fact]