The LA3 Opus review process note was right: the contract both sides
implement lived only in orchestrator prompts, which is exactly the drift
mode the pin exists to prevent (and it produced the paths-key CRITICAL).
The schema, field rules, probe-mode discriminator, and status vocabulary
are now a binding plan section; amendments change this text first,
implementations second. Ledger: LA3 fix round dispatched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus dual-lens review of cb6502c8 passed with six findings; this lands
the fix round:
1. headless-portability.yml: AcDream.Platform src/tests join both path
triggers and the presentation-free build/test arrays — the moved XDG
tests run on ubuntu-latest again (they had fallen out of every Linux
lane).
2. acdream-architecture.md: AcDream.Platform gets its own layer block;
Runtime may-reference clause updated (the guard changed in cb6502c8,
its human-readable twin had not).
3. PlatformDependencyBoundaryTests: the BCL-only contract (zero
project/package references) is now enforced, not just observed.
4. memory/project_linux_graphical.md canonical seam renamed.
5. Plan LA0 recon corrected: the K0 Headless guard was never the guard
needing amendment (it asserts Headless own refs); Runtime own-refs
guard was — the commit did the right thing, the plan text now says so.
6. App declares its AcDream.Platform reference explicitly per its own
convention instead of riding transitivity.
Platform.Tests: 4 passed (3 moved + the new guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher (LA3/LA4) needs the XDG/Windows path contract
(ApplicationPathSet/IApplicationPathEnvironment) without pulling in any
gameplay assembly. Move it out of AcDream.Runtime into a new BCL-only
AcDream.Platform project so the launcher-side Launcher.Core project can
reference it directly per the campaign plan (docs/plans/2026-08-14-launcher-campaign.md,
LA0). Namespace renamed AcDream.Runtime.Platform -> AcDream.Platform;
code is otherwise byte-identical (no logic changes).
AcDream.Runtime now carries a ProjectReference to AcDream.Platform and
re-exports it transitively, so App and Headless keep resolving the type
without a direct reference and K0's Headless single-ProjectReference
guard (HeadlessAssemblyReferencesOnlyTheRuntimeProject) stands unchanged.
The sibling Runtime dependency-boundary guard
(RuntimeProjectDeclaresOnlyApprovedProjectDependencies) does assert
Runtime's own project-reference set, so it needed a deliberate,
documented addition of AcDream.Platform to its expected list.
Moved tests/AcDream.Runtime.Tests/Platform/ApplicationPathSetTests.cs to
a new tests/AcDream.Platform.Tests/ project (namespace
AcDream.Platform.Tests) referencing only AcDream.Platform. Registered
both new projects in AcDream.slnx.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's first click on a mapping button killed the client: the live
dialog catalog's wait root 0x31 carries retail class type 0x19 (WaitDialog),
which DatWidgetFactory left unmapped, so the root built as a plain
UiDatElement and RetailWaitDialogView's ctor threw out of UiButton.OnClick
into the render loop. The unit test missed it by standing the confirmation
fixture (type 0x13, mapped) in for the wait root — the structural-false-
negative class again. Pins: DatWidgetFactoryTests theory for both dialog
root types, plus an installed-DAT UiDialogRoot/0x3D/0x3E assertion in the
env-gated keyboard probe. OpenCaptureInstructions now converts a dialog
construction failure into its contracted 0-return (log + capture refused,
retail's own OpenMapWarnDialog failure shape) instead of crashing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three findings from the user's first Configure Keyboard look (OP8 gate,
2026-08-14), each root-caused against the named retail decomp:
- #394 row-caption font: the synthesized action-label UiText never set
DatFont and fell to the debug bitmap font. The authored row template
(0x21000009/0x1000002F, retail UIOption_ActionKeyMap) carries FontDid
0x4000000A (18px serif) — Bind now takes resolveTemplateFont and applies
the template's own authored font, resolved once per template pair.
- #395 key captions: raw enum spellings ("Shift+ShiftLeft") replaced by the
port of CInputManager_WIN32::GetNameFromKey @0x00687F40 /
GetNameFromKey_Internal @0x00687800 (RetailKeyNames): DAT string-table
override by DIK-name hash (key enum 4 -> 0x2300000A, meta enum 5 ->
0x2300000B, delimiter enum 3 -> 0x23000007 — GetDIDByEnum category 4,
live-probed), else the OS keyboard layout's own key name ("SKIFT") via
PlatformKeyNameProvider (Win32 GetKeyNameTextW — register row AD-96 for
the DirectInput-vs-GetKeyNameText adaptation), else the DIK-suffix
spelling. Bare modifier-key bindings show only the key name.
- #396 capture feedback: clicking a mapping button now opens retail's
instruction dialog (InitiateBinding @0x004899D0 -> OpenMapWarnDialog
@0x00488A00): a type-2 WAIT dialog on retail's MapWarn queue key
0x10000001 with ID_ActionKeyMap_MapInstructions (0x23000004, ACTION
variable interpolated), closed on key hit or ESC through the capture
callback; capture is not armed if the dialog cannot open, matching
retail. New RetailWaitDialogView (wait root 0x31 — same authored
popup/message pair 0x3D/0x3E as the confirmation root, live-DAT probed)
behind a shared IRetailDialogView presenter seam.
Probe evidence (env-gated, kept):
KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings.
Register: AD-96 filed. Gate script OP8 section updated (step 4 rewritten;
the "pressed/active state is enough" contract is retired).
Full Release solution suite green (13,424 passed / 4 skips).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
staged-item trading marker
- Cancel text: ClientTradeSystem::Handle_Trade__Recv_CloseTrade
@0x0056DE30 shows "The trade has been cancelled." UNCONDITIONALLY
(every close reason) as 0x1A ClientLocal - the yellow top-center
SpewBox line. Wired at the router's onTradeClose beside ApplyClose;
the string lives in ClientTextRefusals with its citation.
- Staged-item marker: retail's mechanism decoded end-to-end - the
UIItem prototype (catalog 0x21000037) authors overlay child
0x10000438 (sprite 0x06001DAE, the green frame + corner trade icon),
bound @0x004E18FC and SetVisible(tradeState != 0) @0x004E2420;
gmSecureTradeUI::AddItem @0x004CA801 sets
ACCWeenieObject::SetTradeState(1) on YOUR staged items. Ported as:
UiItemSlot.ShowTradeOverlay + TradeOverlaySprite (drawn over the
icon), set on the trade window's self-grid cells; and
RuntimeTradeState now borrows the canonical object table and
maintains ClientObject.TradeState (1 at stage, 0 at remove/failure/
reset/close/clear) - which also brings the ALREADY-PORTED placement
policy's "You cannot move an item while it is being traded" refusal
to life (its input field previously had no live producer).
Runtime 1,626, App 4,992/3 - green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
root cause for every dead interaction) + retail's Total Items caption
The round-2 probes nailed it: the request seam fired for BOTH open
paths (use AND drag - "drag-release pick" -> "drag-on-player" ->
"request"), but no open-cmd, no wire-open, and no LiveCommandBus
drop-warning ever printed. MountSecureTrade captured
_bindings.Options.CommandBus() ONCE at mount time - the pre-session
surface whose Publish routes into a null route silently. CommandBus is
a Func for exactly this reason; the social mounts resolve it inside
each lambda. Every trade command - open (use + drag), accept (the
"unpressable" Trade button - the click FIRED, the publish died),
Clear All, close, and drop-on-grid staging - died on that one captured
bus. All six lambdas now resolve the Func per call.
Also: ID_SecureTrade_TotalItemsLabel probe-verified token-free
(fragments ["Total Items: ", ""], one ITEMS variable 0x004E8A23) and
composed via ResolveTemplate - the count texts read retail's exact
"Total Items: N". AD-95 RETIRED same-day.
The pre-feature stub-toast test row (drag-on-player option-on expecting
"Secure trade is not open.") now pins the SecureTradeRequested seam
instead. App suite 4,991/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authored gmSecureTradeUI window, and both retail open paths
Three-lane research first (docs/research/2026-08-14-trade-lane{A,B,C}):
retail gmSecureTradeUI decode, the byte-exact ACE/decomp/holtburger
three-way wire agreement, and the acdream seam map (which found both
open paths ALREADY classified by the ported policy - OpenSecureTrade on
Use-a-player, StartSecureTrade on drag-item-onto-player with the
DragItemOnPlayerOpensSecureTrade option - dead-ending at a stub toast).
- Core.Net: TradeRequests builders (0x1F6-0x204, retail's CM_Trade
senders byte-checked against ACE's readers; the ACE-discarded
AcceptTrade echo carries zero-count item lists - AD-94), corrected +
completed inbound parsers (0x1FD-0x208; the old AddToTrade parser
missed the SIDE dword, TradeFailure missed the reason), delegate-hole
registrars, six WorldSession sends. 10 golden-byte tests.
- Runtime: RuntimeTradeState, the third sibling J-owner (fellowship/
allegiance shape): session-scoped, clears at generation reset (new
stage Trade=14), staged teardown stage 11 (Identity/EntityObjects
shift 12/13, TeardownStageCount 14 - the FA2-era per-stage-flag test
caught the mapping exactly as designed), combined ownership ledger,
event routing with ACE's wrong-initiator RegisterTrade landmine
honored (partner = whichever guid is not mine). 7 conformance tests.
- App: SecureTradeUiController binds the dedicated authored LayoutDesc
0x2100000D (root 0x1000007A - gmSecureTradeUI::PostInit's exact ids):
partner name/status/count/grid, the authored 'Trade' accept toggle
(accept <-> decline withdraw), 'Clear All' (ACE clears BOTH sides -
surfaced honestly), the X close, drop-on-your-grid staging, per-mode
accept cues (partner icon's authored Highlight state + Trade button
Selected latch). Mounted via the vendor recipe (nine-slice chrome,
hidden until RegisterTrade). ItemInteractionController's two policy
arms now raise SecureTradeRequested instead of the stub toast; the
drag path queues the dragged item until the window registers
(ClientTradeSystem::AttemptToTradeItem @0x0056DF80's shape).
Register: AD-94 (accept-echo zero-count lists), AD-95 (numeric-only
count texts pending template verification).
Suites: App 4,990/3, Core.Net 905, Runtime 1,626 - all green. The
panel itself is user-gate acceptance (two-client connected trade), the
#372-class lesson: fixture-green alone is not acceptance for a mount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
machine + UiText per-state string swap (user retail gate)
The state side already worked (FriendsState replaces the full entry and
bumps Revision on 0x0021 OnlineStatus updates; the parser reads full
FriendData for every update type). The UI side had two gaps, exposed by
the installed-DAT row-template probe (template layout 0x2100005D root
0x10000519):
- The row authors TWO cells: the LEFT name text 0x1000051A whose
Online (0x10000054) / Offline (0x10000055) PassToChildren states
cascade into the RIGHT status grandchild 0x1000051F, which authors
per-state 'Online'/'Offline' strings AND per-state colors (retail's
green Online). The controller's FindDeepest binding wrote the NAME
into the STATUS cell (the deepest text IS the status grandchild) and
never flipped the state machine - so the status column never showed
or updated anything.
- UiText had no per-state authored-string swap: ApplyDatState switched
sprite + color per state but never the 0x17 string. Ported now
(second consumer of the mechanism after the powerbar caption):
DatWidgetFactory pre-resolves each state's authored string;
TrySetRetailState swaps the line, colored by the SAME state's
authored 0x1B.
SocialFriendsPageController now binds the name to its own cell and
flips the authored Online/Offline state per friend on every
Revision-driven rebuild - the cascade renders the status cell exactly
as retail's gmFriendsUI does, green Online included.
App suite 4,989/3 skips (new per-state swap conformance test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gate; supersedes CH round-1 item A)
The user's retail description matches the decomp exactly:
charge_jump @0x005281c0 has NO grounded check - it refuses only 0x49
(CanJump encumbrance) and 0x48 (fallen/crouch-family forward commands).
Pressing jump while airborne begins the powerbar and charges normally.
The 0x24 "You can't jump while in the air" comes exclusively from the
RELEASE path (ClientCombatSystem::DoJump @0x0056B110 ->
CMotionInterp::jump -> jump_is_allowed, whose airborne 0x24 our port
already carries test-pinned). A charge held through landing executes a
normal jump on the grounded release.
PlayerMovementController's input orchestration now mirrors
CommenceJump/DoJump:
- Press edge: ChargeJump() decides; a refused charge (0x48/0x49)
reports and never begins the bar (retail's jump_pending stays 0).
The invented airborne press-edge 0x24 report (CH user-gate round 1
item A - added when the press/release split was not yet known) is
deleted; CommenceJump's in-air fallback text is unreachable with a
faithful charge_jump.
- Hold: accumulates grounded OR airborne; leaving the ground mid-charge
no longer force-fires the jump.
- Release: fires jump(); an airborne release refuses 0x24 there.
Tests: the round-1 press-edge test is replaced by two release-semantics
tests (airborne release reports once; held-through-landing grounded
release jumps silently). Runtime 1,619, App 4,987/3, Core jump family
159.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gate; the DAT authors it)
The raw-property probe settles it exactly as the user reported: the
powerbar caption's mode STATES author their OWN justification (0x14
Enum=0x3 = Right on JumpMode/MeleeMode/MissileMode) while the element
default stays centered. ElementInfo.HJustify only ever read the
effective DEFAULT state, so the earlier "authored Center" conclusion
measured the wrong state.
The meter's absorbed state-label entry now carries the state's own
authored alignment (state 0x14 wins, element-level HJustify as the
fallback, ElementReader's same enum mapping), and the caption draw
aligns accordingly - 'Height' sits at the bar's right edge, retail's
placement. Live vitals labels keep their centered draw.
App suite 4,987/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail authors ONE caption element per bar with per-mode state strings,
switched by a PassToChildren state cascade (gmPowerbarUI::
RecvNotice_BeginPowerbar @0x004DA730 sets 0x10000042 Jump / 0x10000043
Melee / 0x10000044 Missile / 0x10000045 DDD; installed-DAT probe
confirmed every string + PassToChildren flag).
- Jump bar (user gate): the floaty powerbar's caption child (0x10000035:
JumpMode 'Height', authored HJustify=Center over the bar) was dropped
by UiMeter's child absorption. The stateful-fill meter build now
absorbs it into per-state labels; TrySetRetailState latches the
caption and OnDraw shows it when no live Label provider is bound.
JumpPowerbarController's existing JumpMode flip now surfaces 'Height'
with zero controller changes. The mount gained the string resolver the
Build call never passed.
- Combat bar (user gate): label 0x10000052 authors 'MeleeCombat' ->
'Power' and 'MissileCombat' -> 'Accuracy'; the controller latched the
MELEE string once at bind. CombatUiLabels now resolves both authored
strings and OnCombatModeChanged sets the mode's string - switching
live when swapping melee <-> missile weapons in combat. Also fixed
the mode-state flip target: the states live on the BASIC PANEL
(0x1000005C, PassToChildren), not the layout root (Hide/ShowDetail
only) - the old _root flip was a silent no-op.
New env-gated ACDREAM_PROBE_POWERBAR layout probe (kept, house
pattern). App suite 4,987/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
move-cursor border band
- Empty-state text (round 3): the literal-\n split was correct but each
authored LINE rendered as one clipped run. Retail word-wraps each
authored line within the element extent (its GlyphList draw - the
same wrap RetailConfirmationDialogView already uses). Multiline
authored text now wraps through UiText.WrapWords against the widget's
LIVE width/font/color (cached per width+font+color, re-read per call).
Single-line authored labels keep their one-run shape - re-wrapping
every label is a client-wide change no gate asked for.
- Move cursor (round 3): "the frame won the hit-test" is not a border
test - windows whose interior is not fully covered by children (the
inventory panel's empty regions) resolve those pixels to the frame
too. The border is now a geometric 8 px band along the window's outer
edge, AND the frame must win the hit-test so border-adjacent content
keeps its own cursor. Resize-edge claim still takes precedence;
whole-surface dragging unchanged.
App suite 4,984/3 skips (new BuildText_MultilineAuthored wrap test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refused-drop yellow notice
Item 4 (confirmation dialogs missing text + names): the missing retail
mechanism was StringTable template substitution - an entry is N+1 literal
fragments interleaved with N named variables, composed by
StringTable::GetString @0x004300D0 (no-metalanguage branch @0x004303B7).
ACE sends the bare player name for types 1/4; retail's OWN CLIENT wraps
it. Ported as DatStringResolver.ResolveTemplate (PLAYER hash 0x05506DA2,
the exact compute_str_hash space; Chorizite stores the variable hashes
directly):
- Server-driven type 4 -> ID_Fellowship_FellowshipRequest, type 1 ->
ID_Allegiance_AcceptSwearConfirmation, injected into
GameplayConfirmationController; null resolve falls back to the bare
wire message, never invented English. The 2/3/5/6 " Continue?" family
never consults the composer.
- Local Swear/Break/Kick: the bind-time fragment-0 latch (which showed
the dangling "Do you wish to swear to ") is replaced by click-time
ResolveTemplate with the target's name.
All five templates verified token-free in the installed DAT - this is
NOT a StringTableMetaLanguage port (AD-81's engine caveat stands).
Item 5 (refused drop shows nothing; retail shows yellow top-center
text): the prevRequest latch was ALREADY ported (InventoryTransactionState);
what was missing was the consumer. InventoryTransactionState now raises
RequestFailed(request, weenieError) when a 0x00A0 clears the latch;
ItemInteractionController composes ServerSaysAttemptFailed @0x0058EAE0's
"The <item> can't be <verb>" (verb table + suffix map ported verbatim in
Core's InventoryFailureMessages, NAME_PLURAL for merge/split) and routes
it as LogTextType 0x1A ClientLocal -> the SpewBox, retail's yellow
top-center line. The dispatcher's second leg (@0x0055B342) also runs:
outside the 7-code exclusion set, WeenieErrorMessages resolves per-code
text/destination; 0x426 AttunedItem has no row in either place beyond
the verb line - faithful single-line output.
Register: AD-85 narrowed to its numeric-field item, AD-81 amended (the
token-free interleave is now ported; meta-token engine + FormatName
remain), AD-93 filed (wire-guid-match vs retail's latched-guid
preference; no Move/Wield latch kinds).
Tests: +2 InventoryTransactionState failure-latch, +5 ResolveTemplate
(constructed StringTable fixtures), +1 composer injection, +1 end-to-end
refused-drop line. Core 4,697/1 skip, App 4,983/3 skips.
Research: docs/research/2026-08-13-confirm-and-weenie-error-display.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
empty state, retail amber row selection
- Move cursor (user-directed, ALL windows): HoverWindowMove now
advertises only where the window frame element itself wins the
hit-test - its border pixels; interior points resolve to content
children. Matches retail's Dragbar-chrome-only move cursor.
Whole-surface dragging still works, it just does not advertise.
- Empty-state text (round 2): the DAT stores the LITERAL two-character
escape backslash-n (probe-verified - the dump printed the escape, not
line breaks), so the round-1 newline split never matched. Escapes are
normalized before splitting in DatWidgetFactory authored text.
- Selected fellow amber (user: "check retail"): probe-verified - the
row name band 0x10000282 AUTHORS the retail selected-row art
(DirectState 0x06001450 + Highlight 0x06001451, the amber). Selection
flips the band's ActiveState to Highlight; no invented tint.
App suite 4,976/3 skips. Confirmation-text + refused-drop-notification
research (the round's items 4-5) lands as part 2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One user-ordered batch across the FA social panel + world selection.
Every root cause was probe-proven before the fix (new
ProbeSocialClickRouting in SocialPanelLiveMountProbeTests - production
window mount + real UiRoot hit-tests + a synthetic click):
1. STUCK CHECKBOXES (fellowship x4, allegiance x1, "always checked /
can't change any options"): the authored checkboxes carry DAT
ToggleBehavior, so UiButton SELF-FLIPS Selected at MouseUp - the old
handlers read the flipped value and wrote the ORIGINAL back, snapping
every click to where it started (the probe recorded (id, oldValue)).
Fix: SuppressSelfToggle (the CH6a/b mirror discipline) + derive the
next value from the STORE; the per-tick seeding mirrors it back.
2. UNCLICKABLE ROSTER ROWS ("only get the move window cursor"): the row
name text is display-text ClickThrough=true, which the hit-test walk
skips regardless of HandlesClick - the wired OnClick was unreachable.
Fix: UiText.OnClick assignment now clears ClickThrough (central,
documented); the stats text gains the same select handler so most of
the row's width selects the fellow.
3. TRUNCATED EMPTY-STATE ("You do not belong... To create MISSING"):
the authored string resolves COMPLETE (three sentences) but embedded
'\n's rendered as one clipped line. DatWidgetFactory now splits
authored strings into one Line per newline, with the provider still
re-reading DefaultColor live (the state-color contract - caught by
BuildText_AuthoredLineTracksStateFontColor).
4. FELLOW NAMES WHITE (user-directed): the AD-82 invented leader-gold +
selection-blue tints are deleted; names always white (register row
narrowed).
5. ALLEGIANCE HEADER LABELS: bare "0"/"0" -> "Followers: N" / "Rank: [N]"
(user-specified format; the full retail StringInfo composition stays
AD-85's gap), monarch block matching.
6. FRIENDS/SQUELCH LIVE (AD-79 mostly retired): Add friend (name box ->
0x0018, retail clears the box - Request_AddFriend @0x0048D240),
Remove (row-click selection -> 0x0017), Appear Offline (CharacterOption
0x27 via the immediate 0x0005 auto-save, ACE pushes FriendStatusChanged
to your friend-of list), Squelch Character/Account add-by-name
(0x0058 guid0/type AllChannels + 0x0059) and Remove for the selected
row. The wire beneath (builders, WorldSession sends, Runtime commands,
parsers) existed end-to-end since J4.1/FA1 - this is panel wiring only
(docs/research/2026-08-13-social-wire-completion.md, committed here).
Send Tell stays inert (not in the order; AD-79's remainder).
7. WORLD SELF-SELECTION ("clicking my own char should select myself"):
retail has NO self-exclusion (CPhysicsPart::Draw @0x0050D823 arms
every physobj; RecvNotice_SmartBoxObjectFound @0x004E5BAE selects
unconditionally) - the includeSelf gate was an unregistered
divergence, now removed on both the left-click and right-click paths.
Element roles were probe-measured, never guessed (Add 0x10000514 /
Remove 0x10000515 / Send Tell 0x10000516 / Appear Offline 0x1000052C /
name field 0x1000051B; Squelch: field 0x10000540, Remove 0x10000547,
Squelch Character 0x1000054B, Squelch Account 0x1000054C).
Register: AD-79 mostly retired, AD-82 narrowed. Known remainder, filed
not hidden: the fellowship page's authored 600px content vs the 362px
viewport leaves Dismiss/Assign-Leader below the fold until the window is
resized taller (probe-measured; candidate follow-up).
Tests: Checkbox_Click fact rewritten to the mirror contract (both
directions), monarch-followers label updated, includeSelf expectation
updated, probe extended (click routing, synthetic click, action-widget
role dump). App suite 4,976/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
position memory, unified monitor, maximized restore; AD-92
Dual-lens Opus review of e56aa511 (reports committed under
docs/research/). The consolidated corrections:
- Mechanism M1 (load-bearing): on Windows, Silk's GLFW error callback
QUEUES exceptions on a static list instead of throwing - they detonate
later at window close, which is exactly #388's original two-stage
crash shape. catch(GlfwException) was dead code here and a failed
SetWindowMonitor "succeeded". Success is now judged by the NATIVE
POST-CONDITION (GetWindowMonitor after the call) on both enter and
exit; the catches remain only for the throwing platforms.
- M2 (both lenses): same-mode fullscreen re-apply is a no-op BEFORE any
native work (new IDisplayModeSwitcher.CurrentFullscreenMode). Every
Display-backed Config row applies per change - sliders per DRAG TICK -
so without this every tick while fullscreen re-issued a real
display-mode change.
- M3/M5 (both): the remembered windowed placement is process state (two
target instances exist - startup and live-save); a fullscreen boot now
exits through either instance to the real placement, not the (60,60)
literal.
- M4 (both): the switcher resolves the WINDOW'S monitor (attached
monitor when fullscreen, else IWindow.Monitor's index into the GLFW
array - the same monitor DisplayModeCatalog enumerated), primary only
as a last resort; the offered-list/switch-target mismatch is gone.
- Blast M2b: the offered-mode validator falls back to the SAME static
ladder the dropdown falls back to - Full Screen is no longer a
permanent silent no-op on catalog-less hosts (the switcher's own
monitor-mode-list check remains the hard guard).
- Blast M3: a windowed pick on a MAXIMIZED window restores it first
(Size writes are silently ignored while maximized; the deleted
WindowState=Normal write used to do this incidentally). New
IWindowedSizeSurface.IsMaximized/Restore.
- Mechanism M5: no silent bail-outs - the unparseable-resolution
fullscreen path logs, and the failure line no longer claims "staying
windowed" when the state is unchanged (#392 noted inline).
- Q1 nit: one cached Glfw wrapper (per-call GetApi allocated + took a
native refcount); IsFullscreen/CurrentFullscreenMode guarded.
- AD-92: highest-refresh-for-WxH + refuse-and-log versus retail's
pass-through-and-error ForceDisplayResolution.
Known-open tail, filed not hidden: #392 (persisted-flag divergence on a
refused enter - needs an apply-result seam); the mechanism report's
pacing-refresh WATCH rides the same seam.
Tests: +3 (same-mode no-op, unparseable-while-fullscreen refusal,
maximized restore-before-write). App suite 4,975/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 5+6 of the display block, one coherent unit (they share the state
machine the goal's dual review covers).
GlfwDisplayModeSwitcher (#376) ports retail's fullscreen semantics -
Device::ForceDisplayResolution @gmClient::Init 0x004047af is a REAL
video-mode change - through native glfwSetWindowMonitor on the same
IWindow.Native.Glfw handle path #348's cursor cache proved. Primary
monitor (retail's primary display device); refresh = the monitor's
highest for the picked WxH; the windowed placement is remembered for the
exit path; every failure is a no-throw (bool, reason) result.
SilkRuntimeDisplayWindowTarget.Apply (#388) becomes the state-aware
machine: fullscreen target = validated native mode switch (mode must be
in #391's DisplayModeCatalog - an offered mode is supported by
construction, making the "Graphics mode not supported" crash class
unreachable from the dropdown); windowed target while fullscreen = the
native exit (which sets the client size itself); plain windowed pick =
the proven #387 size write. A raw Size write NEVER happens against a
fullscreen window - on GLFW that is a video-mode request, and an
unsupported one was the exact unhandled-GlfwException that killed the
user's 2026-08-13 session. The old Silk borderless WindowState path is
deleted from the apply. New IWindowedSizeSurface narrows the window
dependency so the machine is unit-testable (FakePacingSurface idiom).
Live-verified on this machine (goal-sanctioned automated run):
display: fullscreen mode switch 1920x1080@300 -> framebuffer resize
event 1920x1080 -> vulkan: swapchain recreated 1920x1080 ok=True ->
graceful close, desktop mode restored.
Tests: 5 state-machine facts (validated switch/never-size-write,
unoffered refusal, failed-switch usability, native exit, plain windowed
write). App suite 4,972/3 skips. Gate script sections D4-D6 written
(black-screen-risk steps flagged). Dual Opus review of the pair follows
as its own round.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decomp-first per the block's rule: the research doc
(docs/research/2026-08-13-retail-ui-display-change.md, committed here)
pulled retail's actual mechanism before any code. A display change runs
UIElementManager::RefreshEvent @0x0045C530 ->
UIElement::UpdateForParentSizeChange @0x00462640, which unconditionally
re-applies every floating window's own clamping MoveTo override
(x = max(0, min(x, parentW - selfW)) - top-left priority, oversized
windows pin to 0), then broadcasts global message 0xE whose sole
listener reloads the per-resolution auto layout. No proportional moves,
no resets; retail saves layouts only via @saveui.
Port: RetailWindowLayoutPersistence.ClampAllToScreen() is the cascade
clamp (no store I/O; _restoring suppresses the per-move save so a live
drag-resize cannot write settings.json per frame), and
RetailUiRuntime.Draw carries a two-step screen-size edge detector:
change frame -> clamp; first stable frame -> one
RestoreAll(saveBack:false) per-resolution reload (the 0xE analog; no
lazy save-back, matching retail's save-only-on-command). The login
restore path already used retail's exact clamp math (Apply) - the live
trigger was the missing half, which is precisely the stranding the user
reported.
Deliberate deviation, register AD-91: retail's gmFloatyChatUI windows
have NO clamp and can strand; the block's requirement ("UI windows must
stay reachable") clamps every registered window uniformly.
Tests: 5 new persistence facts (clamp/top-left-pin/no-move/no-save-on-
clamp/no-save-on-live-reload). App suite 4,967/3 skips. Gate script
section D3 filled in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-lens Opus review of 7e0c1303 (reports committed under
docs/research/). The law, gate, and vertical application are CONFIRMED
at instruction-byte level against the PDB-paired acclient.exe (the BN
text FPU-elides this whole area); the fix round addresses the findings:
- Blast MUST-FIX 1: real schema migration instead of a hand-edited dev
file. SettingsStore v2->v3: a pre-v3 display.fieldOfView was the
applied vertical FOV in degrees; v3 means retail's m_fGameFOV.
LoadDisplay migrates on read - the untouched old default 60 maps to
the retail default 90; a deliberate other value preserves its visible
16:9 framing (x (16/9 - 0.1)), clamped to the registered [10,160];
the next save stamps v3 and migration never reruns. The dev
settings.json hand-edit was reverted so the migration owns it.
- Blast MUST-FIX 2 / mechanism M2: the Field of View now applies LIVE on
Save (retail: Render::GRPCallback_OnRenderPreferenceChanged @0x0054d999
-> SmartBox::SetDefaultFov). RuntimeSettingsTargets gains the camera
graph and applies through ApplyDisplayWindowState - the update-phase
seam, deliberately NOT the render-phase preview path (the review's
WATCH-3 cull-vs-raster landmine).
- Mechanism M1 -> register row AD-90: retail's divisor aspect runs
through the Render.AspectRatio preference (ComputeAspectForViewport
@0x0054f150, (w/h) x pref x 0.75) - exactly raw w/h at the registered
default, which is what acdream assumes; retail's NaN-through-the-gate
quirk (M3) is folded into the same row as deliberately not reproduced.
- Docs: RetailFieldOfView now cites the decisive vertical proof
(D3DXMatrixPerspectiveFovLH fovy slot @0x0059ab71), the unconditional
SmartBox::RenderNormalMode site, and M4's exact horizontal numbers
(89.0/83.9/80.6 deg); the Config FOV row comment updated to LIVE.
- Blast WATCH 4 disposition: the 15 replay-harness PI/3 constants stay -
they are CAPTURE-TIME camera parameters for recorded fixtures, not
production framing; changing them would invalidate the replays.
Tests: +6 SettingsStore migration facts, +1 live-apply fact.
App suite 4,962/3 skips; UI.Abstractions 922.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-directed (2026-08-13): "we should only support modern resolutions.
Not any old format." New DisplayModeCatalog enumerates the window's
monitor (Silk IMonitor.GetAllVideoModes) once at GameWindow load and
curates via a pure, tested rule: modern widescreen families only
(16:9/16:10/21:9/32:9 within 2.5%), at least 1280 wide, must fit the
desktop (an impossible windowed pick is not offered - the measured
3840x2160-on-2560x1440 silent clamp class), desktop mode always
included, refresh-rate duplicates collapsed, ascending order.
The Config Resolution row consumes the catalog through two new optional
Bind parameters; its Defaults value becomes the desktop's own mode.
Fixture/headless callers keep the static preset ladder, which now drops
800x600 and is pinned by test to pass the same curation rule (the OP6 S4
"default must be re-selectable" invariant holds on both paths).
Deliberate retail deviation, register row IA-22: retail listed the
adapter's complete enumeration including 4:3 legacy modes and authored
800x600 as the Config default (gmConfigUI::InitOptions
SetDefaultValue(0x03200258); gmClient::Init @0x004047af). The catalog is
also the designated fullscreen mode-switch validation source for
#376/#388 - an offered mode is supported by construction.
Tests: DisplayModeCatalogTests (8 - filter/clamp/dedupe/sort/ultrawide/
desktop-inclusion/fallback-consistency); ConfigOptionsPageControllerTests
row-12 default updated. App suite 4,961/3 skips; UI.Abstractions 916.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail's world-camera FOV is not a constant: the applied vertical FOV is
m_fGameFOV / (viewportAspect - 0.1), recomputed on every aspect or
game-FOV change (CreatureMode smartbox sites 0x00452b2f/0x00453b14),
gated by Render::SetFOVRad's open (0, pi) acceptance (0x0054b2d0 -
rejected results keep the previous FOV). m_fGameFOV defaults to pi/2 =
90 degrees (0x00454649) and is what the Field of View option sets in
degrees (0x00451e6a; registered range [10,160] default 90 -
gmClient::InitUIPreferences @0x004035b0). Net effect: the horizontal
view stays ~85-90 degrees across aspect ratios; wide screens trim the
vertical slice instead of ballooning the sides.
acdream hardcoded FovY = pi/3 = 60 degrees on all four world cameras,
aspect-independent, and the Config slider wrote raw vertical-FOV
degrees. New: RetailFieldOfView (the law + gate, decomp-cited),
CameraController.GameFovRadians + SetGameFov + one ApplyProjection
chokepoint recomputing every camera on SetAspect/SetGameFov/
EnterChaseMode/RestoreState; ApplyFieldOfView now feeds the law;
DisplaySettings.Default.FieldOfView 60 -> 90 (the retail registered
default; the stored number changed MEANING with this commit).
The same seam closes a second latent bug the 2026-08-13 "squished" gate
report exposed: SetAspect only ever updated Orbit/Fly - the CHASE
cameras (the ones the player looks through) kept their creation-time
aspect across every mid-session resize, drawing the world at the old
shape stretched onto the new viewport.
The paperdoll camera stays outside the law by design (retail portrait
mode is UseSharpMode, not smartbox - DollCamera's own doc).
Tests: RetailFieldOfViewTests (golden law values at 4:3/16:9/21:9, the
constant-horizontal property, the rejection gate, controller propagation
incl. chase attach/restore + rejected-law aspect-still-propagates);
DisplaySettingsTests + RuntimeSettingsControllerTests updated to the new
semantics. App suite 4,953/3 skips; UI.Abstractions 916/0. AD-89 retired
in this commit; user settings.json migrated 60->90 by hand (stale
pre-port default).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: resolution picks (and window drags) stretched the image
instead of changing the pixel count. Root cause: Campaign V slice V11
deleted the GL viewport target and left a null target, assuming the
driver's OUT_OF_DATE/SUBOPTIMAL acquire/present results would drive
swapchain recreation on resize. That is driver-dependent and
spec-insufficient — this machine's Windows AMD driver keeps presenting
the stale-extent swapchain scaled to the new window indefinitely, so
OnFramebufferResize only ever updated the camera aspect while every
pass (UI included) kept rendering at the old extent.
Fix: SwapchainRecreateViewportTarget implements the existing
IFramebufferViewportTarget seam for Vulkan and arms
VulkanGraphicsContext.RequestRecreate() on every resize event; the next
PrepareFrame rebuilds the swapchain at the live FramebufferSize (bursts
collapse to one recreation, stale events cannot install a stale extent,
minimised sizes stay gated by FramebufferResizeController).
Tests: SwapchainRecreateViewportTargetTests (target contract, size-
agnostic arming, null hook, controller-to-target end-to-end with the
minimised gate). Full Debug App suite 4,941/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LandblockStreamer.HandleJob's near-payload check is "fail loud in Debug
builds and strip in Release" (its own comment, with a Debug.Assert at the
check). FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty
feeds a deliberately-buggy far factory to verify the Release strip — so
under Debug the assert fires, the test host's listener turns it into an
exception, and the job publishes Failed BY DESIGN. The test asserted the
Release outcome unconditionally and therefore failed on every full Debug
App run (found 2026-08-13 during the #385 session; every campaign gate
runs Release, which is why it never surfaced). It now asserts the Failed
result + assert message under DEBUG and the strip under Release. Verified
green in both configs; full Debug App suite 4,937/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User gate report (Campaign OP happy-testing round, 2026-08-13): every
Config-tab dropdown drew its text gold + left-aligned and its popup a
fixed 6 rows regardless of item count. All three were unmeasured styling
divergences — the authored data (new probe menuprobe3, live DAT) says:
- button label child 0x10000355: fontColor white, hJustify=Center
- row template 0x1000035A: fontColor white, hJustify=Center
- popup ListBox 0x10000358: edge-docked L=T=R=B=1, the authored condition
arming retail UIElement_Menu::RecalculatePopupSize @0x0046caf0 —
popup resizes to the ListBox's summed content height, uncapped
(0x0046e5f4..0046e66c via ResizeScrollableArea's 0x32 broadcast)
UiMenu gains three opt-in properties (ButtonTextCentered,
ItemTextCentered, PopupSizeToContent) plus retail Open @0x0046cc42's
empty-list gate; chat + vendor keep the class defaults, so their shipped
behavior is untouched. ConfigOptionsPageController.ApplyMenuChrome wires
all four corrections for the 8 Config menus with the probe citation.
The same probe found vendor's authored popup ListBox is ALSO docked while
our vendor dropdown ships G5's fixed 6-row window — filed as #386 +
register row AD-88 (UNCLEAR: the G5 retail screenshot and the decomp
mechanism conflict) instead of silently reworking a user-gated surface.
The "resolution change resizes the window" observation from the same
report is #374's designed windowed-mode behavior (display-mode switching
is #376/#377) — no change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second live gate run exposed a real environmental hazard: this shared
ACE dev instance has a THIRD player character online (+Je, guid
0x50000001), and after @teleallto it ended up nearer to the Leader bot than
the actual Recruit bot (+Horan, 0x5000000B). RuntimeFriendlyTargetQuery.
FindClosestOtherPlayer — "nearest ANY other player" — picked +Je, and the
fellowship recruit sent to it obviously never completed (confirmed live:
WaitRecruited/WaitForRecruit both timed out, both bots quarantined and
gracefully logged out cleanly).
RuntimeFriendlyTargetQuery.FindPlayerByName resolves the nearest player
whose streamed name matches exactly, with 3 new conformance tests
(preferring the named player over a closer stranger, returning null when
absent, and case-sensitivity/hidden/no-draw/self rejection).
FellowshipAllegianceGateCoordinator (AcDream.Headless.Policies) is a small
same-process, no-locking (single update thread) carrier for the Recruit
bot's own discovered character name — set by its own HeadlessSessionHost
the instant CharacterList selection resolves it, which IS D8's "discover it
live" mechanism, not a hard-coded value. Constructed once per
HeadlessProcessHost and threaded through HeadlessBotPolicyFactory.Create
into the Leader policy, which now name-matches instead of taking whichever
player entity happens to be closest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RuntimeHostileTargetQuery only classifies hostile monsters (via
CombatTargetPolicy.IsHostileMonster) — the FA6 two-bot fellowship/allegiance
headless gate needs the OTHER bot's server guid as a FRIENDLY target instead.
RuntimeFriendlyTargetQuery.FindClosestOtherPlayer mirrors the hostile query's
shape exactly (same hidden/no-draw filtering, same landblock-absolute
distance metric), substituting the retail PWD-bitfield IsPlayer bit (0x8,
via the existing EntityCollisionFlagsExt.FromPwdBitfield decoder) for hostile
classification. TryGetName resolves the streamed WeenieHeader name for
reporting/logging.
4 new conformance tests mirror RuntimeHostileTargetQueryTests's fixture
pattern: cross-landblock distance, hidden/no-draw/self/non-player rejection,
null-without-player-or-target, and unresolved-guid name lookup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA5 mechanism review found the offline-vassal name-grey
(OfflineNameColor) is an invented visual: retail's UpdateVassalsData
@004924c3 writes the vassal name with no colour change, and the offline
cue is EXCLUSIVELY the authored 0x100004AA marker (already wired,
SetVisible per online state). Removed OfflineNameColor; the vassal name
always renders in the normal white. The Allegiance page now carries NO
invented tint (unlike Fellowship's registered leader/selection tints).
Pinned by Allegiance_OfflineCue_IsTheMarkerOnly_NameStaysWhite (marker
visible iff offline, name always white). AD-82's FA5 addendum corrected
(it had described the now-removed grey as 'covered by the marker'); AD-86
count corrected seven -> nine.
Full Release suite: 13,297 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA5. The Allegiance page (0x10000291) goes from FA3's
empty-state shell to fully live, wired against FA1's parser and FA2's
RuntimeAllegianceState/IRuntimeAllegianceCommands (both already shipped
the full command surface, including SetUpdateSubscription).
CF-1 (the corrected data subscription): 0x001F AllegianceUpdateRequest,
not 0x027B, is the panel's data source (0x027B/0x027C are text-only chat
per FA2 MF-2). Wired at retail's three arming points -- Bind's PostInit
attempt (almost always a pre-world no-op), the post-world EnteredWorld
seam (RedeclareAfterWorldEntry, UNCONDITIONAL -- does not check the
current latch, matching retail's own PlayerDescReceived arm and avoiding
the exact MF-3-REOPEN bug class FA4 hit for 0x00A6), and the visible
branch (SetPageVisible, edge-triggered, folded into
SocialPanelController's existing window-shown+active-tab conjunction
alongside Fellowship's 0x00A6).
Monarch/patron/self blocks: per-relationship empty-state gate (fix-round
SF-7) replacing FA3's coarse HasProfile-only gate -- the monarch block
hides when there is no monarch OR the monarch is the viewer; the patron
block hides when there is no patron OR the patron is the monarch (in
which case the monarch block's 0x10000490 sub-block reveals and its
label swaps to PatronSlashMonarchLabel). Field sources decompiled fresh
from gmAllegianceUI::UpdatePlayerData/UpdateMonarchData/UpdatePatronData:
0x10000251 is the ALLEGIANCE's own name (not the viewer's), follower
counts are TotalVassals/TotalMembers-1 directly off the wire, and the
"experience passed up" text (0x10000492, doubled -- scoped FindDescendant
under each of its two parents) is the viewer's own CpTithed under the
monarch/patron blocks and each vassal's own CpTithed in their row.
Vassal roster: flat list built via UiTemplateListBox.FlushPreservingScroll
in the FA4 roster-diff pattern (guid-set diff, in-place update on an
unchanged set), rendering in the bindings' own already-reversed order.
Swear/break/kick: each opens a local confirmation dialog
(RetailDialogFactory via ShowConfirmation) before sending, mirroring
retail's MakeSwearConfirmationDialog family -- Swear targets the WORLD
selection (via the same ClientObjectTable name resolver
ToolbarRuntimeBindings.ResolveName already uses), Break targets the
current patron, Kick targets the panel-local selected vassal row (no
world-selection sync for Allegiance, unlike Fellowship). The
server-driven "accept incoming swear" (ConfirmationType 1) needed no new
code -- GameplayConfirmationController already handles every type
generically; a new test verifies it explicitly.
Runtime/composition plumbing: DeferredGameRuntimeStateCommands gains
Allegiance{Swear,Break,Kick,SetUpdateSubscription}; SocialRuntimeBindings
gains the Allegiance view/command projections; SocialPanelController.
Callbacks.AllegianceSnapshot widens to a full
SocialAllegiancePageController.Bindings record, mirroring FA4's
Fellowship widening.
Tests: SocialPanelControllerTests.cs gains 10 tests covering the SF-7
gate (4), roster population, swear/break/kick wiring (3), and the CF-1
subscription arming points (2); GameplayConfirmationControllerTests.cs
gains the type-1 verification test.
Also extends SocialPanelLiveMountProbeTests.cs (production-mount
assertions: scoped 0x10000492 resolution, the vassal row template, the
checkbox, confirmation-dialog string resolution, and a full production
Bind() pass) -- not yet run against live DATs in this worktree (no
Documents/Asheron's Call present here).
Release build green; full solution suite 13,296 passed / 4 skipped / 0
failed (13,300 total), up from FA4's 13,285/4/0 baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA4 fix round's MUST-FIX 3 placed the 0x00A6 reconnect re-arm at the
wrong lifecycle point (re-review 8bbceff5): ResetSessionTransientUi runs
via the SessionDialogs reset stage BEFORE _inWorld=true, so SetPanelOpen
(world-gated, Validate requireWorld:true) returned Inactive and published
nothing — yet _pageVisible was latched true anyway, so no later hook
re-declared and fellow vitals stayed frozen for the whole new session.
The unit test passed only because the fake recorded unconditionally.
Two-part fix, both retail-faithful mechanisms not suppressions:
- SocialFellowshipPageController.SetPageVisible advances the edge-trigger
latch ONLY when the declaration is Accepted (published), so a dropped
pre-world send leaves the latch clear and a later attempt retries.
- ResetSessionDeclaration (pre-world) now ONLY clears the latch; the new
RedeclareAfterWorldEntry fires from the LiveSession EnteredWorld seam
(wired via RestoreLayout, idempotent if a persisted layout already
re-showed the page) so a still-open Fellowship page re-declares 0x00A6
in world and vitals resume.
Regression pins that actually catch it (the prior test could not):
- SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld
(widget-level root, world-gated fake);
- Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset +
Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen (panel-level,
world-gated). RED-verified: reintroducing the pre-world declaration
fails the reconnect test.
Full Release suite: 13,286 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SocialPanelLiveMountProbeTests wrote the four checkbox labels and the two
Open/Close captions to the console with no assertion, yet the FA4 ledger's
live-DAT paragraph cited them as verified -- the same finding FA3's own
mechanism SF-3 raised for a different table ("printed but never asserted
-- deserves a real assertion, not just a hope"). Now asserts each label is
non-null/non-empty and the two captions equal the exact retail strings
"Open"/"Close". Env-gated (ACDREAM_PROBE_LIVE_MOUNT=1, real installed
DATs) -- inert in this session's build/test run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md)
established that retail's client reads neither IgnoreFellowshipRequests nor
FellowshipAutoAcceptRequests on the fellowship-invite path -- both are pure
server-side filters with no client consumer, exactly like the two
allegiance bits they were always meant to parallel. Their claimed consumer
(RetailUiRuntime.TryAutoRespondToFellowshipInvite) is deleted in a sibling
commit this fix round. Both rows revert from Live to StoreOnly.
Mechanism review SF-8 additionally found FellowshipShareLoot's claimed
consumer -- "a second live checkbox surface on the fellowship page" -- is
not a consumer at all: nothing in acdream reads the stored value back
(FormatStatsText uses snapshot.ShareXp only; the 0x00A2 Create builder
carries shareXP alone), and the live-DAT dump confirms its checkbox is a
child of the NOT-in-fellowship frame -- invisible whenever you actually
have a fellowship to loot-share within. A second EDITOR of a value is not
a CONSUMER of it under AD-78's own "drives nothing observable client-side"
definition. FellowshipShareLoot reverts too.
Only FellowshipShareXP survives as genuinely live -- the Create-flow click
reads it directly as the sent shareXP wire bit. Net: 35 (pre-FA4) -> FA4
shipped 31 -> fix round reverts three -> 34 of 50 dimmed / 16 live, ONE
net un-dim from the pre-FA4 baseline, not four. Updated the class doc's
derivation table, the conformance test's ExpectedStoreOnlyIds set, and the
31/19 count assertions to 34/16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 3 -- SocialFellowshipPageController.SetPageVisible is edge-triggered
on a bool that survives a generation reset unchanged while the panel stays
open, so a reconnect never re-sends 0x00A6 and fellow vitals freeze for the
rest of the new session. SocialPanelController.ResetSessionDeclaration
clears the fellowship page's latch (SocialFellowshipPageController.
ResetPageVisibleLatch, this commit's counterpart) and re-evaluates the
existing "window shown AND Fellowship active" conjunction, wired into
RetailUiRuntime.ResetSessionTransientUi -- a seam that already runs on
every generation reset. A still-open Fellowship page re-declares; a closed
or other-tab page correctly stays silent.
SF-4 -- SocialPanelController's constructor subscribed an anonymous lambda
to UiTabPanel.ActivePageChanged with no way to remove it; Dispose only set
a flag. A tab switch after Dispose still reached
UpdateFellowshipPageVisibility and issued a Runtime command, since Tick's
own _disposed guard doesn't cover this event path. Stored the handler as a
field and unsubscribe it in Dispose.
Also adds the panel-level D4 conjunction test mechanism SF-5 flagged as
missing (the only prior D4 test exercised the PAGE controller's own
edge-trigger directly, never SocialPanelController's "window shown AND
Fellowship active" logic or its ActivePageChanged subscription).
Per docs/research/2026-08-12-fa4-review-mechanism.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MUST-FIX 1 -- D5's percentage conversion rounds where retail truncates.
gmFellowshipUI::UpdateFellowStats @0x0048ECC9 forms pct*100.0f on the x87
stack then calls _ftol2 (MSVC's round-to-truncate helper), never
MathF.Round. The stored floats for 6 and 8 fellows are 0.44999998807907104
and 0.3499999940395355 (byte-read from the PDB-paired binary), so retail's
own products truncate to 44/34, not 45/35 -- and (int)(pct*100f) alone does
not fix it, since 0.45f*100f already rounds UP to exactly 45.0f in single
precision. Fixed as (int)((double)pct * 100.0), forming the product the
same wider-than-single-precision way retail's x87 does. Pinned with new
[InlineData] cases for both sizes.
MUST-FIX 4 -- gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 (the
world->panel arm of retail's two-directional selection coupling) was never
ported; only the panel->world arm (SelectFellow) shipped. Selecting a
fellow in the WORLD left Dismiss/Assign-Leader disabled and showed no row
highlight. SyncSelectionFromWorld/SetSelectedFellow reproduce the
observable contract (button-enable + a row tint) against this
controller's own guid-keyed row dictionary instead of porting retail's
generic ListBox SetAttribute_InstanceID/SetSelectedItem primitive (scoped
disposition recorded at register row AD-82).
Also in this pass over the controller:
- SF-1: cache the fellowship-name LinesProvider; only reassign on an
actual name change (was allocating once per Tick, even while hidden).
- SF-2/SF-3: track true membership in _memberGuids, independent of which
rows finished building. Fixes an unbounded DAT-locked rebuild retry
when a row template permanently fails to build, and fixes Recruit's
"already a fellow" check reading render rows instead of membership.
- N-0: the Open/Close caption now flips optimistically on click, matching
retail's pre-toggle-before-server-echo (lane B feature 11).
- N-1/N-2/N-3: doc-only notes on the meter-child-text gap, the max>0
guard, and Tick's two-read non-atomicity.
- ResetPageVisibleLatch: the fellowship-controller half of MUST-FIX 3
(see the SocialPanelController commit for the panel-level half).
Per docs/research/2026-08-12-fa4-review-mechanism.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RetailUiRuntime.TryAutoRespondToFellowshipInvite auto-declined/auto-accepted
fellowship invites based on IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
before the dialog ever reached GameplayConfirmationController. Byte-verified
across Handle_Character__ConfirmationRequest @0x005640A0,
RecvNotice_FellowshipRequest @0x00490880, and MakeFellowRequestDialog
@0x00490620 (whose only guard is m_fellowRequestContext) plus a whole-file
sweep of both option accessors: retail's client reads neither bit on any
confirmation path. ACE filters both bits server-side, so the interceptor was
dead code against a correct ACE and actively harmful against a drifting one
(IgnoreFellowshipRequests defaults true, so it would silently swallow real
invites with no dialog and no chat line).
HandleConfirmationRequest now routes every confirmation type, including 4,
straight to the generic controller -- exactly like retail. No tests existed
for the deleted interceptor (nothing to remove); added a test proving the
type-4 dialog renders the server message verbatim (not "Continue?"-suffixed)
and sends accept/decline through the generic path.
Per the corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roster: SocialFellowshipPageController now builds one row per fellow
from the authored template (0x21000030/0x10000281, live-DAT verified),
diffing the member GUID set on each revision-gated Tick -- an unchanged
set updates every row's bound widgets in place (no ListBox mutation, so
scroll position is untouched by construction); only a real join/leave/
disband triggers a rebuild, via UiTemplateListBox.FlushPreservingScroll
(FA3 carry-forward 1, both the widget-level fix and the controller-level
diff). Health/stamina/mana meters bind Fill+Label; the leader's name
tints gold (lane A's row template has no dedicated leader marker, so
this is a flagged adaptation, not a ported mechanism). Row-click
selection (SelectFellow) drives Dismiss/Leader targeting and the world
selection (SelectionChangeSource.Social).
D4: SocialPanelController now tracks "is the social window shown AND is
Fellowship the active tab" via UiTabPanel.ActivePageChanged +
OnShown/OnHidden, and calls SetPageVisible on every transition, which
sends 0x00A6 (idempotent, no-op while disconnected) -- the prerequisite
ACE gates its 0x02C0 vitals stream on.
Create flow: the inline name field (0x1000026F, an authored Editable
UiField -- live-DAT verified) gates the Create button's enabled state
exactly like retail (empty name = disabled = the whole refusal
mechanism, no separate error text); FellowshipShareXP's live value is
read at click time.
Actions + confirmations: Recruit/Dismiss/Quit/Disband/AssignLeader/
SetOpen all route through DeferredGameRuntimeStateCommands (new
Fellowship* methods) rather than a raw WorldSession send, so Quit
correctly picks up RuntimeFellowshipState's leader hand-off rule.
Button enable states port gmFellowshipUI::UpdateButtons verbatim. The
Open/Close button's caption swaps between the two DAT-resolved strings
cached once at Bind (never per-tick -- DatCollection is not safe to
touch unprotected from the render loop). RetailUiRuntime intercepts a
type-4 confirmation request before it reaches the generic
GameplayConfirmationController: IgnoreFellowshipRequests auto-declines,
FellowshipAutoAcceptRequests auto-accepts, neither set falls through to
the existing dialog machinery unchanged (D6).
D5 display: the per-fellow stats line uses retail's byte-decoded
even-split percentage table verbatim (1.0/.../.3111111/.28, default
0.0); the proportional branch omits the percentage rather than
inventing a formula (no acdream ExperienceToRaiseLevel table exists
yet). Both StringInfo variable substitution (row/stats/vitals text) and
ACCharGenData::FormatName (create-flow name canonicalization) are
unported prerequisites, so row text renders as plain numeric composites
-- register rows AD-80/AD-81 (docs commit).
D7: un-dims IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
(consumed by the D6 auto-decline/accept) and FellowshipShareXP/
FellowshipShareLoot (consumed by Create + the page's own second
checkbox surface) on the Character tab -- 4 of 35 store-only rows
promoted to Live (31 remain dimmed).
Carry-forwards from the FA3 re-review, folded into this slice's
contract:
- UiTemplateListBox.FlushPreservingScroll -- preserves scroll offset
across a rebuild instead of resetting to 0 (Flush's existing
contract, unchanged, for Friends/Squelch).
- RowTemplateResolver -- the FA3 caching row-template resolver
extracted from a MountSocialPanel local function into its own
hermetically-testable class; now shared by Friends/Squelch/
Fellowship's row families.
- Friends/Squelch scrollbars now resolve via the built
UiTemplateListBox.ScrollbarElementId (DAT property 0x72) instead of
a hardcoded literal, matching ConfigOptionsPageController's own OP6
precedent.
- The Fellowship roster path never advances its revision latch on a
partial resolver failure until the NEXT real membership change --
never a per-frame retry loop.
Live-DAT verified (ACDREAM_PROBE_LIVE_MOUNT=1, extended
SocialPanelLiveMountProbeTests): the name field builds as UiField, all
11 buttons/checkboxes resolve, the row template's 5 checked fields
resolve to the right widget types, every checkbox label/tooltip and the
Open/Close captions resolve to real retail strings ("Open"/"Close"),
and a full production-path Bind() against live DATs produces zero
"not found" warnings.
App tests: +30 (7 UiTemplateListBox/RowTemplateResolver unit tests, 23
SocialFellowshipPageControllerTests covering roster diff/rebuild,
button enable rules, checkbox wiring, create-flow gating, D4
idempotency, and D5 formatting) plus 2 CharacterOptionsPageController
counts updated for the D7 un-dim (35->31 dimmed, 15->19 live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IRuntimeFellowshipView.GetMembers() gives the fellowship panel a way to
enumerate the whole roster (TryGetMember alone needs the guid first,
which a UI roster build doesn't have yet). Implemented on
RuntimeFellowshipState.FellowshipView as a materialized snapshot under
the same lock every other read there uses.
SelectionChangeSource.Social covers a fellowship-roster row click
(gmFellowshipUI's list-selection arm calls the same
ACCWeenieObject::SetSelectedObject primitive every other selection
origin uses -- lane B docs/research/2026-08-11-fa-panel-structure.md
§6.2/§2.8).
Runtime tests: +4 (RuntimeFellowshipStateTests.GetMembers_*).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism SHOULD-FIX 4: SocialFellowshipPageController's class doc said
0x1000026B holds "all three visible option checkboxes" — the fixture and
the decomp both authors FOUR (Ignore Fellowship Requests / Auto-Accept
Requests / Share XP / Share Loot, 0x10000270-0x10000273). Corrected.
Mechanism SHOULD-FIX 9: SocialPanelRowText.FindDeepest's doc promised
"the deepest UiText descendant" but the implementation returns the LAST
match in pre-order traversal order, which only equals the deepest when
the subtree is a single chain. Both real row templates ARE single
chains today, so behavior is unaffected — the doc now describes what the
code actually does instead of a stronger guarantee it doesn't implement.
Blast SHOULD-FIX 5: UiTemplateListBox.Flush()'s doc only mentioned the
ContentHeight reset; UiScrollablePanel.ClearContent() also resets scroll
position to 0, which the sibling UiItemList.Flush() (same method name,
different semantics) does NOT do. Documented explicitly, including the
UX cost this creates for a scrolled-in Friends/Squelch roster once its
scrollbar is wired (this fix round's blast MF-1) — flagged for whoever
revisits Friends/Squelch scrolling next rather than silently fixed as an
unasked behavior change.
Mechanism SHOULD-FIX 3: SocialPanelLiveMountProbeTests printed two
headline findings (the 0x10000492-authored-twice count, page exclusivity
after ActivateTabBehavior) without ever asserting them — a future
importer regression collapsing/dropping an instance, or breaking
exclusivity, could not fail this test. Both are now real assertions
(Assert.Equal(2, passupCount); exactly one page Visible and it is
Allegiance).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA3 dual-lens review fix round.
Blast MUST-FIX 1: SocialFriendsPageController/SocialSquelchPageController
never wired their ListBox's own sibling scrollbar (Friends
0x10000517->0x10000518, Squelch 0x1000053E->0x10000543) — the two lists
had NO scroll driver at all (no wheel fallback exists on
UiScrollablePanel), making any roster past the visible extent completely
unreachable, not just awkward to scroll. Both controllers now wire
`scrollbar.Model = listBox.Scroll` scoped to their own page root, exactly
like the four existing UiTemplateListBox consumers
(Character/Chat/Config/KeyboardConfig). New tests prove the wiring AND
that a >panel-height roster is actually reachable through it.
Blast SHOULD-FIX 2/3: the Friends/Squelch resolver re-ran
LayoutImporter.ImportInfos (a full DAT tree walk) under the shared DAT
lock on EVERY row, every revision, even while the panel was closed —
RetailUiRuntime.MountSocialPanel's TemplateResolver now caches each row
template's ElementInfo the first time it is resolved and never
re-Imports for that template id again. SocialPanelController additionally
gates the Friends/Squelch Tick-driven rebuild on the panel's own
visibility via the (previously unused) IRetainedPanelController
OnShown/OnHidden hooks, so no DAT-locked rebuild work runs at all while
the panel is closed. SocialFriendsPageController/SocialSquelchPageController
also now only advance _lastRevision after every row resolves — a
transient resolver miss no longer latches an empty roster until the next
server-side change; it retries on the next Tick instead.
Mechanism SHOULD-FIX 8: SocialPanelController.Tick() now returns early
once disposed, matching every other J-slice teardown discipline (it was
being ticked unconditionally forever since RetailUiRuntime never nulls
the field).
Mechanism SHOULD-FIX 1: IsShowingAllegiance's doc claimed the F3/F4
close-on-second-press semantics were "retail's Toggle-action semantics" —
re-derived and confirmed NO retail OnAction consumer exists for either
action anywhere in the binary. Relabeled as acdream's own
OpenSpellbook-precedent convention; no register row added, following the
same no-row precedent OpenSpellbook and every other non-toolbar Toggle
panel already sets. Unknown filed as U11 in the panel-structure research
doc's §8 table.
Tests: 5 new (2 scrollbar-wiring pins, 2 long-roster-reachable-via-
scrollbar, 1 hidden-panel-does-not-rebuild/shown-panel-catches-up); 2
existing tests updated for the new visibility gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RetailLayoutFixtureGenerator dumps LayoutDesc 0x2100006E slot
0x1000018F to the committed social_panel_2100006E_1000018F.json
fixture, closing lane-A unknowns U3/U4/U6/U7/U10 with real DAT data
(geometry/media/fonts/base refs, the fellowship empty/full frame
containment, the panel/page P0x57 properties, the row-template arrays).
- SocialPanelLiveMountProbeTests exercises the PRODUCTION mount path
against the live DATs (ACDREAM_PROBE_LIVE_MOUNT=1): the tab host
resolves as UiTabPanel with its 4-entry table, all four pages resolve,
the fellowship frame pair and allegiance signature elements resolve,
0x10000492 is confirmed authored twice under the allegiance page, and
every tab button caption is non-empty (the #375 resolver class).
- SocialPanelControllerTests pins the fixture-driven conformance: the
real (coordinator-addendum-correcting) tab table and default entry,
the Fellowship/Allegiance empty-state gates, Friends/Squelch row
population and revision-driven rebuilds, and the D1 INERT-button
contract (AD-79) via
FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler.
- RetailPanelCatalogTests gains the SocialPanel id/window-name/
Mounted-not-Toolbar pins (lane A §6.1: no toolbar button).
Note: the ACDREAM_REGENERATE_UI_FIXTURES=1 run used to produce the new
fixture also touched keyboard_config_21000009.json and
options_2100002B.json on this machine (unrelated installed-DAT drift,
likely from local DAT-editing tooling) -- both were reverted to HEAD
before this commit; only the new social panel fixture is included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remaining SHOULD-FIX findings from the FA2 mechanism/blast reviews:
Mechanism SF-3/SF-4 -- RuntimeFellowshipState.ApplyUpdateFellow now ports
Fellowship::RecalculateEvenXPSplitting @0x005B92E0 (called from retail's
AddFellow/UpdateFellow/RemoveFellow on every upsert/removal, but never
from a full update -- that carries the server's own authoritative flag
verbatim, lane B 6.2) and Fellowship::AddFellow @0x005B9480's
locked/departed admission gate (a brand-new guid is refused while
_locked unless it appears in the 0x02BE field-8 _fellows_departed table
within 900s, @0x005B94A5). ApplyFullUpdate now stores update.Departed
instead of discarding it. A TimeProvider dependency (defaulting to
TimeProvider.System, matching the RuntimeCharacterOptionsState precedent)
makes the 900s grace window testable.
Mechanism SF-5 -- RuntimeAllegianceState's TryGetMember/TryGetPatron/
GetVassals now reuse ClientCommandResponses.AllegianceProfileLookups
(promoted private -> internal, AcDream.Runtime added to Core.Net's
InternalsVisibleTo) instead of re-implementing the retail walk a second
time.
Mechanism SF-6 -- RuntimeStateCheckpoint's Fellowship/Allegiance
parameters are no longer trailing-optional. `default(RuntimeFellowshipSnapshot)`/
`default(RuntimeAllegianceSnapshot)` zero-init Name/AllegianceName to
null, and C# does not allow a non-constant `new(...)` as an optional
parameter's default value (CS1736) even when the struct declares an
explicit parameterless constructor -- so the only way to guarantee a
non-null default was to make the parameters required. Both snapshot types
still gained an explicit parameterless constructor for callers that want
an empty-but-safe `new()`.
Blast SF-4 -- LiveSessionEventRouterTests gains
FellowshipQuit_RoutesSelfGuidToClearAndOtherGuidToRemove, wiring real
RuntimeFellowshipState/RuntimeAllegianceState owners through the one
production registration site and dispatching a real 0x00A3 envelope for
both a self-quit and an other-quit -- the one non-trivial lambda in the
slice (the self-guid source that decides "remove one member" vs "clear
the whole snapshot") was previously untested; every other router test
defaults Fellowship/Allegiance to null.
Blast SF-5 -- RuntimeFellowshipState.ResetSession dropped its disposed
guard to match the precedent its own doc comment names
(RuntimeInventoryState.ResetExternalContainer,
RuntimeCommunicationState.ResetNegotiatedChannels -- both bare delegations
with no disposal guard); the reset transaction is retryable and disposal
is terminal, so a throwing guard could never converge on retry.
RuntimeAllegianceState.ResetSession (new this fix round) matches the same
shape from the start.
Blast SF-7 -- IRuntimeAllegianceView.GetVassals' per-call List<> allocation
is now documented as an intentional exception to the file's "Snapshot +
TryGet*, no allocation" view convention (C# cannot yield-return from
inside a lock).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two MUST-FIX findings from the FA2 mechanism/blast reviews
(docs/research/2026-08-12-fa2-review-mechanism.md,
docs/research/2026-08-12-fa2-review-blast.md):
MF-1 (mechanism) -- RuntimeAllegianceState survived a generation reset,
contradicting retail (ClientAllegianceSystem::OnEndCharacterSession
@0x00569FA0 tail-calls AllegianceProfile::Clear at the same boundary
Fellowship already clears at), contradicting the precedent it cited
(RuntimeCharacterOptionsState.ResetSession clears-and-relatches, it does
not persist), and pinned by a test asserting the wrong behavior. Fixed:
RuntimeAllegianceState.ResetSession() clears the profile and drops
HasServerSeed; a new RuntimeGenerationResetStage.Allegiance stage runs it
on every generation reset, mirroring RuntimeFellowshipState exactly.
RuntimeGenerationResetTests' FellowshipClearsAtResetButAllegianceSurvivesReconnect
inverted to FellowshipAndAllegianceBothClearAtGenerationReset.
MF-2 (mechanism) / blast MF-2 -- 0x027C AllegianceInfoResponse fed the
Runtime allegiance owner (self-gated). Retail's own handler for 0x027C
(CM_Allegiance::DispatchUI_AllegianceInfoResponseEvent @0x006a7470) unpacks
into a stack-local profile destroyed on return; the consumer
(Handle_Allegiance__AllegianceInfoResponseEvent @0x0056a1d0) only prints
AddTextToScroll lines. Retail's panel is fed exclusively by 0x0020
AllegianceUpdate. The removed seeding also fabricated
RuntimeAllegianceSnapshot.Rank (0x027C carries no rank field) on any
client whose first allegiance message was a self @allegiance info query.
Fixed: dropped ApplyInfoResponseSelf, the onAllegianceInfoResponseSelf
delegate hole, and the self-gate; 0x027C is text-only again, matching
retail and the pre-FA2 shape.
Also covers blast SHOULD-FIX 1 in the same edit to LiveSessionEventRouter.cs:
the fellowship/allegiance delegate holes are now passed conditionally on
the owner being supplied, so GameEventDispatcher.GetUnhandledCount reads
correctly for callers without an owner (bare-ChatLog tests, a future
partial host) instead of silently reading 0 for 9 event types whose parse
result was discarded.
RuntimeAllegianceState.cs and the two owners' Apply* mutators also move
their ObjectDisposedException.ThrowIf checks inside the lock they already
take (mechanism SHOULD-FIX 2) -- the prior check-then-lock shape let an
inbound event on the decode thread race Dispose on the host thread and
repopulate state after _disposed = true, permanently falsifying
CaptureOwnership().IsConverged at teardown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both LiveSocialSessionBindings construction sites updated together
(LiveSessionRuntimeFactory.cs, HeadlessSessionHost.cs) so the single
GameEventWiring.WireAll registration site serves both hosts identically
(the K-slice unification). CurrentGameRuntimeCommandAdapter implements
IRuntimeFellowshipCommands/IRuntimeAllegianceCommands over the App
command bus (LiveSessionCommandRouter gains 12 new *RuntimeCmd records
+ registrations + LiveSessionCommandBindings send delegates), mirroring
DirectGameRuntimeCommandAdapter's direct-session shape including the
identical Quit leader-hand-off rule read from RuntimeFellowshipState.
CurrentGameRuntimeAdapter (the graphical IGameRuntimeView/
IGameRuntimeCommands composite) exposes the two new views/command
groups. Headless's DirectGameRuntimeCommandAdapter needed no changes --
it already implements both new interfaces from the Runtime-layer
commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two new sibling Runtime owners under GameRuntime per the Slice-J
pattern (D2): RuntimeFellowshipState is session-scoped (a new
RuntimeGenerationResetStage.Fellowship clears it at every generation
reset, matching the ExternalContainer precedent); RuntimeAllegianceState
survives reconnect behind a HasServerSeed-style one-way latch and
participates in NO reset stage (its data persists like a real
disconnect does not sever allegiance membership).
Fellowship: full-update REPLACE, incremental-fellow UPSERT, self-vs-
other quit/dismiss removal (self clears the whole snapshot), disband
clear, and retail's leader hand-off rule for the Quit button
(RequiresLeaderHandoffBeforeQuit -- the current leader quitting WITHOUT
disbanding must send 0x0290 AssignNewLeader before 0x00A3, lane B
§2.5/§3.6).
Allegiance: seeded by AllegianceUpdate (0x0020, always self) and,
self-gated on TargetGuid == playerGuid(), by AllegianceInfoResponse
(0x027C); wraps the FA1-assembled flat AllegianceMemberRecord list
directly (AllegianceTree was deleted at FA1 -- nothing left to wrap).
Both apply the full 8-edit J-owner template: construction + fault
points + Owner/View properties + CaptureOwnership + a new
GameRuntimeTeardownStage pair (FellowshipDisposed/AllegianceDisposed,
stage count 11->13) + RuntimeGameplayOwnershipSnapshot inclusion +
RuntimeStateCheckpoint/trace fields. IRuntimeFellowshipCommands/
IRuntimeAllegianceCommands added to IGameRuntimeCommands and
implemented on DirectGameRuntimeCommandAdapter. No IRuntimeEventObserver
member added (D2) -- consumers poll Snapshot.Revision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the missing WorldSession.Send* link for every FA1 fellowship/
allegiance builder (SendFellowshipCreate/Quit/Dismiss/Recruit/
UpdateRequest/AssignNewLeader/ChangeOpenness, SendAllegianceSwear/
Break/Kick/UpdateRequest) and 15 new GameEventWiring.WireAll delegate
holes covering the 11 S->C fellowship/allegiance events. Delegate holes
(not state-object params) because Core.Net cannot reference
AcDream.Runtime, matching the onCharacterOptions/onConfirmationRequest
precedent.
Fixes a real bug found during implementation: GameEventDispatcher.
Dispatch invokes only the single most-recently-registered handler per
GameEventType (RegisterOwned REPLACES, it does not chain-invoke) --
contradicts the seam doc's "the dispatcher supports multiple owned
handlers per type" claim. A literal second registrar.Register call for
AllegianceInfoResponse would have silently killed the already-live
`@allegiance info` chat-text output the moment a caller supplied the
new self-gated Runtime callback. Both behaviors are folded into the
ONE existing registration instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies both MUST-FIX items and the code-facing SHOULD-FIX items from the
dual-lens FA1 review (docs/research/2026-08-12-fa1-review-mechanism.md,
docs/research/2026-08-12-fa1-review-blast.md):
Mechanism MF-1 / blast SF-2: AllegianceHierarchy::Add @0x005B6E90 wraps its
entire body in `if (_id != 0)` -- a record whose own id is zero discards
the WHOLE message, for both the monarch and a child record, and this is
also what makes treeParent == 0 unconditionally fatal for a non-monarch
record. ReadAllegianceProfileBody now rejects CharacterId == 0 on both
paths; four new boundary tests in AllegianceProfileVersionGateTests.cs
(zero-id monarch, zero-id child, zero treeParent, plus the existing
orphan/self-parent/duplicate trio).
Mechanism MF-2: added the missing 0x001F AllegianceUpdateRequest builder --
the structural twin of the fellowship 0x00A6 this slice already repaired --
with golden-vector tests for both on/off.
Mechanism SF-1 / blast SF-3: UnPack's last act before returning success
forces the monarch's MayPassupExperience to false regardless of the wire
bit or the HasPackedLevel-absent legacy-compat fallback. Ported at the end
of the record loop; the pre-existing HasPackedLevel-absent test moved off
the monarch record (which the new clear makes indistinguishable from "the
fallback never fired") onto a vassal record, and a new test proves the
monarch clear fires even when the wire bit explicitly asks for true.
Mechanism SF-2: removed ParseFellowshipDisband's invented body-length
validation -- retail's DispatchUI_Disband reads only the opcode and never
inspects a trailing body. The parser now always succeeds; the matching
test flips from asserting rejection to asserting acceptance.
Mechanism SF-3: added the D5 `<<1` shareLoot-shape test at the 0x02C0
FellowshipUpdateFellow site -- previously only pinned at 0x02BE, so a
future split of the shared ReadFellow helper could silently reintroduce a
bool read on this leg undetected.
Mechanism SF-5: renumbered the version-gate comments in
ReadAllegianceProfileBody to the true AllegianceVersion enum values
(1-11, matching acclient.h's SpokespersonAdded..ApprovedVassal) instead of
wire-appearance order, which only reached 10 and silently dropped gate 5
(BannedCharactersAdded, which is real but gates nothing in UnPack -- now
called out explicitly). Fixed the stale "lane B §12" citation in
SocialActions.cs to the actual master-table row.
Blast SF-1: pinned the two retail-faithful but user-visible behavior
changes FA1 made to the ALREADY-LIVE `@allegiance info` command --
reversed vassal print order (3-vassal test through
FormatAllegianceInfoLines) and malformed-tree silent-drop (test at the
GameEventWiring registration layer, which is `if (info is null) return;`).
Blast SF-4: fixed a doc comment citing a nonexistent `ConfirmationResponseTests`
class; the actual class is `ConfirmationTripleTests`.
Blast SF-5: cross-referenced the confirmation-triple discriminator's split
representation (ConfirmationType on the response leg only; bare uint on
the two inbound legs production actually reads) at both sites, so FA4
inherits a stated decision rather than an unexplained inconsistency.
Full Release suite: 13,158 passed / 4 skipped / 0 failed (13,162 total),
up from the pre-fix-round 13,149/4/0 (+9 tests this round).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA1, lane C §0/§7.3 DELETE verdict. Verified zero
production references (a repo-wide grep for AllegianceTree/
AllegianceNode/AllegianceMath outside the class's own file and its own
test file returns nothing but docs and an unrelated test-name string
coincidence in ChatChannelInfoTests.cs).
Two defects made this safer to delete than fix:
- AllegianceMath.ComputePassup transcribed retail's passup formula wrong
by roughly 1000x: it computed (50 + 22.5*loyalty) / 291 instead of
50 + 22.5*(loyalty/291) AS A PERCENTAGE (missing the trailing / 100),
and its own unit test locked the wrong value in as correct.
- AllegianceTree's UpsertNode(guid, name, patronGuid, rank) modeled a
patron edge the wire does not carry -- the wire names each record's
TREE PARENT (§4.4), which for ACE's own writer is not always the real
patron (ACE hangs a non-monarch patron directly off the monarch). The
parsed record list plus its treeParent tags already IS the tree
(see ClientCommandResponses.AllegianceProfileLookups, landed in the
companion feat(net) commit this session) -- no separate tree class is
needed. The client also never needs the passup number at all:
_cp_tithed arrives pre-computed from the server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FA1 contract asked to "add the 8 missing fellowship entries lane B
lists" (0x0417-0x041C, 0x04DB, 0x04DC). Re-ran this table's own binary-
sweep methodology specifically for these 8 ids rather than inventing
text for them: a full-text grep of the 1.4M-line
acclient_2013_pseudo_c.txt found ZERO comparisons/case-labels against any
of the 8 anywhere in the retail client, and a manual walk of
HandleFailureEvent's own case-label sequence confirmed the switch goes
straight from case 0x416/0x41d (skipping 0x417-0x41c) and from case
0x4da/0x4dd (skipping 0x4db/0x4dc).
Conclusion: retail's Sept-2013 client has no display text for any of
these 8 ids -- they are intentionally absent from this table, not
overlooked. This contradicts the FA1 contract's premise but not lane B's
own text, which only claimed the ids were "missing" from the table (true)
and that two of them (0x0417, 0x04DB) are on ACE's live send paths (also
true) -- it never claimed retail has text for them. Two of the ids are
therefore live-but-silent gaps against a real ACE server, and acdream's
current no-display behavior for them is ALREADY retail-faithful. Adding
invented English would be exactly the class of mistake SHOULD-FIX 4
(the no-default-case rule this table's Resolve() already implements)
exists to prevent.
Documents the finding at both table gaps and adds a conformance test
(Resolve_FellowshipIdsAbsentFromHandleFailureEvent_ReturnsNoText) proving
all 8 resolve to null text, matching the existing
Format_0x051D_ReturnsNull_NoRetailCaseExists precedent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA1: pure parse functions + typed records only,
UNWIRED (FA2 registers them against the new RuntimeFellowshipState/
RuntimeAllegianceState owners -- see docs/research/2026-08-11-fa-acdream-seams.md
§2).
Fellowship family (GameEvents.cs), field orders from lane B §3.8-§3.13,
guid-first on 0x02C0 per the resolved Chorizite disagreement:
FellowshipFullUpdate (0x02BE), FellowshipUpdateFellow (0x02C0),
FellowshipQuitNotice/FellowshipDismissNotice (S->C 0x00A3/0x00A4),
FellowshipDisband (0x02BF, empty body), and the dead
FellowshipFellowUpdateDone/FellowshipFellowStatsDone (0x01C9/0x01CA,
parse-and-ignore, must never fail per lane B §2.7). ShareLoot is modeled
as a raw uint (D5) -- ACE encodes it two incompatible ways (0x10 in full
updates, <<1 incremental), so `!= 0` is the only safe read, never `== 1`.
Confirmation triple (D6): grepping the tree showed 0x0274/0x0276 already
had typed parsers in Core.Net; 0x0275 (client-authored) already had a
byte-correct builder but no typed representation. Added the
ConfirmationType enum (1 SwearAllegiance, 4 Fellowship, matching retail's
Handle_Character__ConfirmationRequest switch and ACE's enum verbatim) and
ParseConfirmationResponse, completing Core.Net's typed coverage of all
three legs and round-tripping against the existing
ClientCommandRequests.BuildConfirmationResponse byte-for-byte.
Allegiance small events (GameEvents.cs): AllegianceLoginNotification
(0x027A), AllegianceUpdateDone (0x01C8), AllegianceUpdateAborted (0x0003,
declared but never sent by ACE).
The heavyweight AllegianceUpdate (0x0020) extends
ClientCommandResponses.ParseAllegianceInfoResponse (0x027C) rather than a
second parser, per lane C §7.2's explicit reuse verdict -- both messages
now share ReadAllegianceProfileBody, which the discriminating leading u32
(targetGuid vs rank) is read around. That shared reader implements:
- The ELEVEN AllegianceHierarchy::UnPack version gates (lane C §4.2) --
officers/spokesperson-skip, officer titles, the four broadcast
counters, motd/motdSetBy, chatRoomId, bind point, allegianceName,
isLocked, approvedVassal, each behind its own oldVersion threshold.
AllegianceProfileVersionGateTests.cs pins all eleven with a
boundary-crossing pair per gate (N-1 OFF vs N ON), including the
negative proof that version 5 (BannedCharactersAdded) gates nothing
in UnPack.
- The §4.4 tree-assembly rules: a record whose treeParent is not already
in the tree (orphan), equals its own id (self-parent), or duplicates an
id already seen makes AllegianceHierarchy::Add fail, which the whole
parse now mirrors by returning null for the ENTIRE message -- not a
partial tree. Sibling order REVERSES on assembly (each new record is
prepended to its parent's vassal list), so FindVassals now walks
records in reverse wire order; both rules have dedicated tests.
- AllegianceMemberRecord gained the panel-needed columns lane C §7.2
names (rank, level, loyalty, leadership, cpCached, cpTithed, gender,
heritage, MayPassupExperience) with defaulted trailing parameters so
existing 4-arg positional construction sites keep compiling. Officers/
officer titles/bind point are read (so every later field lands at the
right offset) but deliberately left unsurfaced -- ACE always zeroes/
empties them anyway (lane C §5.1), and bind point is a 32-byte Position
the retail chat renderer never uses either; a future panel slice can
extend the record without re-deriving the parse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>