Commit graph

1902 commits

Author SHA1 Message Date
Erik
30fa6ee507 fix #394 #395 #396: OP8 re-gate round — caption font, retail key names, capture dialog
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>
2026-08-14 13:45:55 +02:00
Erik
1528e5693b Merge branch 'main' into claude/latest-commits-cb0c8f
# Conflicts:
#	tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs
2026-08-14 12:52:34 +02:00
Erik
0f15c74147 chore: secure-trade closeout - strip [trade] gate probes, roadmap ledger
The two-client user gate PASSED 2026-08-14 (open both ways, stage with
the retail trading marker, accept/decline, executed swap, Clear All,
cancel text). Every TEMPORARY [trade] probe line from gate rounds 1-2 is
stripped (ItemInteractionController, SelectionInteractionController,
SecureTradeUiController, LiveSessionCommandRouter, WorldSession,
RuntimeTradeState). Roadmap gains the shipped-trade ledger row.

Suites after strip: App 4,992/3, Runtime 1,626 - green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:48:31 +02:00
Erik
ffc73f80bf fix: trade gate round 3 - the mount-time-captured dead command bus (ONE
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>
2026-08-14 12:34:31 +02:00
Erik
067cbea8a5 feat: secure trade with other players - wire, RuntimeTradeState, the
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>
2026-08-14 11:49:13 +02:00
Erik
bee38b0746 docs: file #393 - retail texture-detail options (highres toggle + mip-skip levels)
From the 2026-08-14 highres verification: acdream always runs at retail
MAX texture detail (Textures[0] + unconditional client_highres.dat).
Retail's two knobs (ID_Option_HighResChange gating LoadHighResDat
@0x004FA250; Landscape/Environment TextureDetail as a mip-chain start
index @0x0044C3C8) are recorded with their decomp anchors and the
acdream-side seams. Post-M4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 11:06:55 +02:00
Erik
67fe754dd6 fix: social gate round 2, part 2 - confirmation-dialog sentences + the
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>
2026-08-13 21:10:42 +02:00
Erik
72ceddce2e fix: social panel completion batch (user gate 2026-08-13, "fix all")
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>
2026-08-13 19:30:51 +02:00
Erik
ec2a7b0cce fix #376/#388 review round: post-condition truth, idempotence, one
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>
2026-08-13 18:10:07 +02:00
Erik
229242e1fe docs: file #392 — refused fullscreen enter leaves the persisted flag diverged (blast M4; needs an apply-result seam)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:03:39 +02:00
Erik
e56aa5115c fix #376+#388: real fullscreen mode switching, state-aware display apply
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>
2026-08-13 17:49:17 +02:00
Erik
2153bee247 fix #390: retail display-change UI cascade — clamp + per-res reload
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>
2026-08-13 17:42:50 +02:00
Erik
8463d64311 docs: display-block gate script skeleton — §D1/§D2 testable now, §D3-§D6 pend their slices
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:29:28 +02:00
Erik
d13d63d0a5 fix #389 review round: settings v3 FOV migration + live apply; AD-90
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>
2026-08-13 17:27:15 +02:00
Erik
6b844c142f docs #377: not reproducible on current code — 3/3 clean fullscreen:true launches, evidence + disposition (structural fix rides #388)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:21:40 +02:00
Erik
13d388e5a9 fix #391: curated modern-only resolution list from the monitor's modes
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>
2026-08-13 17:16:09 +02:00
Erik
7e0c130344 fix #389: port retail's SmartboxFOV law; retire AD-89 (display slice 1)
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>
2026-08-13 17:08:41 +02:00
Erik
a1efc8bcb3 docs: file #391 — curated modern-only resolution list (user-directed)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:58:26 +02:00
Erik
4dc079533c docs: file #389 (SmartboxFOV divergence, register AD-89) + #390 (UI stranded off-screen on downscale)
Both from the 2026-08-13 display gate session. #389 carries the full
decomp-verified retail FOV law; #390 requires the retail reposition
mechanism from the decomp before any clamp is implemented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:53:44 +02:00
Erik
c991de38dd docs: file #388 — fullscreen-state video-mode crash + silent resolution-pick no-op (user gate session evidence)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:36:54 +02:00
Erik
a7ae756b44 fix #387: window resize never recreated the Vulkan swapchain (stretch)
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>
2026-08-13 09:05:13 +02:00
Erik
a9b6435f55 fix #385: Options dropdowns — white centered text + size-to-content popup
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>
2026-08-13 08:51:57 +02:00
Erik
028920420d docs: FA7 closeout — Campaign FA CODE-COMPLETE
The retail four-tab social panel (Fellowship & Allegiance) is
code-complete: all six slices landed and reviewed (dual-lens Opus review
-> fix round -> narrow re-review each). Fellowship two-session flow proven
live (FA6 bot gate PASSED). Closeout bookkeeping:
- register AD count 66 -> 67 (AD-87, the deferred allegiance bot gate);
- plan status flipped to CODE-COMPLETE with the OWED connected gates +
  #384 (allegiance-swear ACE non-response) called out;
- CLAUDE.md Current-state gains the Campaign FA paragraph
  (per feedback_claude_md_staleness), pointing at the memory digest.

Owed: the user's connected gates (§FA3-§FA6 of
docs/research/2026-08-12-campaign-fa-test-script.md) and #384's
ACE-console disambiguation. Full suite 13,304/4/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:30:19 +02:00
Erik
01fafe7b37 docs: FA6 — ledger row + gate script §FA6 (fellowship PASSED live, allegiance deferred)
Plan ledger: fellowship two-session automated gate PASSED live 2026-08-12
(five of six runs reproduced the decisive cross-session assertion); the
allegiance bot gate is DEFERRED behind AllegianceGateEnabled=false pending
docs/ISSUES.md #384, with commit citations for every fix this slice landed
(confirmation relay, name-matched proximity, the fellowship-only
finalization).

Gate script §FA6: the fellowship automated-gate recipe + actual PASSED
result (the two-session config, the six proof points per stage, the
literal decisive-assertion log lines), the allegiance deferral writeup,
and a new [TWO-CLIENT] manual step (25) the user's own connected gate can
run to help disambiguate #384 (ACE-side rule vs wire-builder defect vs
harness-specific drop) using two real graphical clients instead of the
testaccount/testaccount2 pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:26:19 +02:00
Erik
022b1844e1 docs: FA6 — file #384 (allegiance swear ACE non-response) + register AD-87
docs/ISSUES.md #384 records the live-run evidence trail (six connected
runs, the 0.005 m distance diagnostic, the confirmation-arrival diagnostic
that never fires) behind AllegianceGateEnabled=false.

docs/architecture/retail-divergence-register.md AD-87 records the honest
divergence this deferral creates: the allegiance half of the FA6 bot gate
is written and wired but unverified end-to-end over the wire, unlike the
fellowship half which is proven live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:24:08 +02:00
Erik
3dde2dc149 docs: FA5 CODE-CLOSED — dual review + SF-1 fix + baseline off-by-one corrected
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:08:41 +02:00
Erik
eac28dc1f0 fix(ui): FA5 mechanism-review SF-1 — remove the invented offline-vassal name-grey
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>
2026-08-12 09:07:58 +02:00
Erik
f12aefe948 docs(fa5): mechanism-faithfulness review — APPROVE-WITH-FIXES (1 SHOULD-FIX)
FA5 mechanism-faithfulness review of 7ed79eaf/bc29a1db/7e394cbf. Verdict
APPROVE-WITH-FIXES. Every high-stakes claim re-derived from the PDB-paired
2013 decomp: CF-1's unconditional 0x001F post-world arm (00490d59 sits
OUTSIDE the busy-count guard), the monarch/patron/self field sources
(UpdatePlayerData/UpdateMonarchData/UpdatePatronData), the SF-7
per-relationship gate, swear=world-selection/no-SetSelectedObject, and the
AD-86 ACE-zeroed-field citations all match retail.

MANDATORY live-mount probe RAN and PASSED against the real installed DATs
(1/1) — the scoped doubled-0x10000492 NotSame assertion and a full
production Bind() with zero "not found" held. FA5 unit suite 36/36 green.

One SHOULD-FIX (LOW): FA5 greys the offline vassal NAME (OfflineNameColor)
— retail's UpdateVassalsData @004924c3 sets the name with no color; the
offline cue is exclusively the authored 0x100004AA marker toggle. Either
drop OfflineNameColor or honestly register it (the AD-82 addendum's
"covered by the marker" framing understates it). Does not block the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:02:01 +02:00
Erik
b6c4a4fa3a docs: FA5 blast-radius review -- APPROVE-WITH-FIXES (1 SHOULD-FIX, 2 NIT)
Suite-accounting SHOULD-FIX: the FA5 ledger/commit cite FA4's INTERMEDIATE
13,285 figure as the baseline and claim +11 net, but FA4 CLOSED at 13,286
(its 'Final full suite' figure) and the real net is +10 (verified per-file
[Fact] counts: SocialPanelControllerTests 22->31, Confirmation 4->5, probe
1->1) -- the ledger's own itemization already sums to +10, contradicting
its +11 headline. End figure 13,296/4/0 is itself correct; documentation
fix only.

Verified clean: all three Callbacks/Bindings construction sites pass the
widened Allegiance binding; every production accessor fed from a real seam;
the 0x001F and 0x00A6 toggles are independent edge-triggered latches with
no cross-talk (38 Fellowship tests green); ResolveWorldObjectName reuses
the Toolbar's ClientObjectTable read and ShowConfirmation is a pre-existing
shared method with no Fellowship collision; @allegiance info/0x027C path
untouched (52 Core.Net + 16 Runtime allegiance tests green); FA5 makes zero
Runtime changes; register 63->66 rows accurate (AD-84/85/86 + AD-82
addendum); NUL-fix correct and no residual control bytes in any of the 11
touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:58:30 +02:00
Erik
bc29a1dbdb docs(fa5): register rows AD-84/AD-85/AD-86 + AD-82 addendum, gate-script SFA5, ledger
Register:
- AD-84 -- Swear button's missing "target is a player" enable-rule gate,
  same class as AD-83's Recruit-button gap.
- AD-85 -- the unported StringInfo variable-substitution engine (AD-81's
  same root cause) extended to the Allegiance page's numeric-only
  followers/rank/experience-passed-up fields and its three local
  confirmation dialogs (verbatim-or-bare-name, never invented).
- AD-86 -- ACE's deliberate zeroing of seven AllegianceProfile/
  AllegianceData fields (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
  gmAllegianceUI, which has no widget for any of them either.
- AD-82 addendum: the vassal-row click-target-only selection shares
  point (3)'s limitation, but NOT the invented leader/selection tints
  (point 1/2) or the Fellowship-only world-selection sync (point 4) --
  Allegiance's list-selection message has no SetSelectedObject call.

Gate script: new docs/research/2026-08-12-campaign-fa-test-script.md
SFA5 section, mirroring SFA4's structure -- the CF-1 subscription steps
(including the reconnect-while-closed MF-3-REOPEN analogue), the SF-7
per-relationship monarch/patron steps, vassal-list steps, swear/break/
kick with their confirmations, the ACE-zeroed-field honesty note, and
full "what to report"/"explicitly not in scope" lists.

Plan ledger: FA5 row filled in against 7ed79eaf with per-item summary,
directly-measured totals (13,296/4/0, +11 net from FA4's 13,285/4/0),
and the two primary-source resolutions this slice needed beyond the
research docs (the self-rank field's live buffed-quality source, and
"your follower count" == _total_vassals, confirmed by a fresh targeted
decompile of UpdatePlayerData rather than inferred).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:41:10 +02:00
Erik
f5bd3e5621 docs: FA4 CODE-CLOSED — MF-3 re-fix (04161def) + re-review (06dbf1cf) closed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:03:42 +02:00
Erik
06dbf1cf8f docs: FA4 MF-3 REOPEN re-fix re-review -- CLOSED (04161def)
The re-fix moves the 0x00A6 re-declaration off the pre-world reset seam
and onto the post-world EnteredWorld seam, and stops the widget latch from
advancing on a dropped publish. Verified in the diff:

- SetPageVisible advances _pageVisible ONLY on RuntimeCommandStatus.Accepted
  (the widget-level root of the REOPEN); a dropped Inactive publish leaves
  the latch clear so the in-world attempt is not deduplicated.
- ResetSessionDeclaration (pre-world) now only clears the latch;
  RedeclareAfterWorldEntry (new) does the re-evaluation, wired through
  RetailUiRuntime.RedeclareSocialPanelAfterWorldEntry into
  LiveSessionRuntimeFactory's EnteredWorld RestoreLayout delegate.

Seam ordering traced and confirmed inverse of the pre-world SessionDialogs
stage: StartCore runs ResetHostBeforeStart (pre-world reset, latch clear)
at :555, then ActivateCommands :639, _inWorld=true :642, and
ApplyEnteredWorld :644 -> LiveSessionHost.ApplyEnteredWorld ->
RestoreLayout delegate -> RedeclareAfterWorldEntry. So SetPanelOpen's
requireWorld gate is Accepted and 0x00A6 publishes on the fresh server.
Idempotent and load-bearing (the social panel isn't state-managed
visibility, so RestoreLayout fires no OnShown edge).

Tests model the world gate (fake returns Accepted only when in-world) and
would fail against pre-fix behavior: the widget test's second attempt is
deduplicated if the latch advances unconditionally; the reconnect test's
DoesNotContain-after-reset fails if the pre-world declaration is
reintroduced (the coordinator's RED-verification). Binary confirmed
post-fix (new tests reference RedeclareAfterWorldEntry); 3/3 new + 58/58
touched classes green. 13,286/4/0 reconciles (+1, 0 deletions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:02:59 +02:00
Erik
04161defd8 fix(ui): FA4 re-review REOPEN — re-declare 0x00A6 from the post-world seam, not the pre-world reset
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>
2026-08-12 07:59:16 +02:00
Erik
8bbceff594 docs: FA4 fix-round narrow re-review -- CLOSED with one REOPEN (MUST-FIX 3)
Re-derived each disposition from the actual fix diffs (290f9b58/5499f058/
df000306/300d8189/55b17e15/1d743277/f041b09b), not the commit claims.

CLOSED (4/5 MUST-FIX, all 9 SHOULD-FIX, all 4 NIT, blast SF-1):
- MUST-FIX 1: (int)((double)pct*100.0) truncation + 6->44%/8->34% pinning
  cases + gate step corrected.
- MUST-FIX 2: intercept deleted, every type routes to the generic
  controller, type-4 dialog test added; type-1 allegiance path unaffected
  (was never intercepted).
- MUST-FIX 4: world->panel selection sync reproduces retail's found/
  fallback arms; AD-82 records the deferred generic UiTemplateListBox
  selection-model port honestly -- minimal-observable-contract, not a
  hidden gap.
- MUST-FIX 5: AD-82/AD-83 well-formed; AD-78 count corrected to 34/16.
- D6/D7/SF-8 dimming (audited from source): 34 dimmed / 16 live is
  correct, not split-the-difference. FellowshipShareLoot has NO client
  value-reader (only an editor/display surface; 0x00A2 sends shareXP
  alone; ACE authors loot server-side) -> dimmed faithful.
  FellowshipShareXP is genuinely read by the Create click -> Live right.

REOPEN (MUST-FIX 3): the 0x00A6 reconnect re-arm is placed at a pre-world
reset seam. ResetSessionTransientUi runs via the SessionDialogs reset
stage at ResetHostBeforeStart / retired-scope teardown -- both BEFORE
_inWorld=true and before command activation for the new generation -- and
SetPanelOpen requires world, so the re-declaration returns Inactive and
nothing is published, yet _pageVisible is still set true and no
post-world-entry hook re-evaluates. The new server never receives 0x00A6
and fellow vitals stay frozen -- the exact bug the fix targets. The unit
test passes only because its fake command records unconditionally.
Recommend moving the re-declaration to an in-world seam (EnteredWorld).

Totals/probe: 109/109 touched App test classes green on post-fix
binaries; live-mount probe PASS 1/1; +13/0-deletion delta and 13,285/4/0
corroborated on the touched projects and by arithmetic (not re-run
end-to-end).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:48:26 +02:00
Erik
f041b09b7c docs(fa4): ledger FA4 row records the fix-round SHAs, per-finding dispositions, and final totals
FA4 row now names all six fix-round commits (290f9b58, 5499f058, df000306,
300d8189, 55b17e15, 1d743277) alongside the original landing's three, and
records: build/test green at every commit; the +13/0-deletion test delta
broken down per file; the final directly-measured 13,285 passed / 4
skipped / 0 failed (13,289 total); every MUST-FIX/SHOULD-FIX/NIT applied;
and the corrected dimmed-row arithmetic (35 -> 31 FA4-original -> 34
fix-round final, net one row). Also corrects item (4) of the
"contradictions/deferrals" list, which called the missing Recruit
is-a-player register row an acceptable inline comment -- MUST-FIX 5 named
that the wrong call; it is now register row AD-83.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:36:03 +02:00
Erik
1d74327771 docs(fa4): fix-round register rows AD-82/AD-83, AD-78 count correction, gate-script SF-7/MUST-FIX-1/3 corrections
Register (docs/architecture/retail-divergence-register.md):
- AD-78: the Character-tab dimmed count had drifted stale through two
  campaigns (still read "35" after FA4 shipped 31; now 34 after the fix
  round's three reversions). Addendum explains the full D6/SF-8 chain.
  Blast review's own SHOULD-FIX 1.
- AD-82 (new): 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 -- MUST-FIX 4's
  disposition plus two items MUST-FIX 5 named as owed rows.
- AD-83 (new): the Recruit button's missing "target is a player" gate,
  previously an inline comment, not a register row -- MUST-FIX 5's third
  item. Section header bumped 61 -> 63 active rows.

Gate script (docs/research/2026-08-12-campaign-fa-test-script.md):
- SF-7: fixed step 3's self-contradiction ("only Quit" then "Disband and
  Open should ALSO be enabled").
- MUST-FIX 3: new reconnect step after the existing close/reopen step.
- MUST-FIX 4: new world-selection step under the recruit/dismiss/quit
  section.
- MUST-FIX 1: new HARD-check step for the 6/8-fellow 44%/34% truncation
  (distinct from the existing SOFT 9-member ACE-divergence note).
- MUST-FIX 2 correction: the old invite steps tested whether acdream's
  CLIENT gates the dialog on the option bits -- a mechanism that never
  existed in retail and no longer exists in acdream. Rewritten to test
  the corrected behavior (the dialog always shows regardless of the
  target's own checkbox state) and to explain what ACE-side filtering
  would look like if the local server implements it, so a tester doesn't
  misattribute ACE's behavior to a client bug.
- Renumbered steps 9-22 to 9-25 to fit the two new steps; updated the
  "what to report" section's step cross-references and rewrote its
  invite/dimming bullets to match the corrected mechanism.

Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D7 addendum: SF-8's further correction (FellowshipShareLoot reverts
  too; only FellowshipShareXP survives as genuinely live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:34:09 +02:00
Erik
e6d97516e5 docs: FA4 review — correct D6/D7 (server-side invite filter) + record D8 account/proximity
D6 asserted the client consumes IgnoreFellowshipRequests/
FellowshipAutoAcceptRequests on the invite path; the FA4 mechanism review
(913e35cd MUST-FIX 2) byte-verified retail reads NEITHER bit client-side
(Handle_Character__ConfirmationRequest @0x005640A0, RecvNotice_
FellowshipRequest @0x00490880, MakeFellowRequestDialog @0x00490620) — ACE
filters both server-side. Same class as the D2 reset-lifetime correction.
D6/D7 corrected in-place with dated addenda: the client-side intercept is
removed, the invite dialog always shows, and the two un-dims revert
(dimmed 31->33). D8 records the user-provided second account
(testaccount2/testpassword2) and the recruit-proximity requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:06:22 +02:00
Erik
e202fbef6e docs: FA4 mechanism review -- reconcile with the concurrent pass at 6849b457
A second mechanism-lens review landed on the same path at 6849b457 while
this one was in progress and was overwritten by 913e35cd. Its text is
recoverable from git and is now cited from a new appendix, its two unique
findings are carried forward, and the two places the reviews disagree are
adjudicated from primary source.

Carried forward:
- SF-9: AD-78's register row still says "35 of 50 rows dimmed" (the D7
  addendum landed in the class doc, not the row's Where column).
- N-0: the Open/Close caption does not optimistically pre-toggle; lane B
  feature 11 records that retail's handler pre-toggles _open_fellow
  locally before sending 0x0291.

Adjudicated:
- _ftol2 vs MathF.Round: 6849b457 filed it a NIT ("round and truncation
  agree on every table value"). That holds for the DECIMAL literals, not
  the stored floats -- 0x007C91D4 = 0.44999998807907104 and 0x007E72BC =
  0.3499999940395355, so retail truncates 44.999998/34.999999 to 44/34
  while acdream rounds to 45/35. Stays MUST-FIX 1.
- D6 invite auto-response: 6849b457 passed it as verified-clean after
  confirming the code matches the plan. The binary says retail has no
  such client-side read on any confirmation path. Stays MUST-FIX 2.

The reviews agree on the reconnect D4 hole, the missing panel-level D4
conjunction test, the leader-tint register omission, and the live-DAT
probe result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 05:03:31 +02:00
Erik
913e35cdb5 docs: FA4 mechanism review -- APPROVE-WITH-FIXES (5 MUST-FIX, 8 SHOULD-FIX)
Findings persisted before any fixer dispatch, per the campaign's §7
review protocol.

MUST-FIX, in the order they were derived:
1. D5's percentage conversion rounds where retail truncates. Byte-decoded
   0x0048ECC9..0x0048ECD8 (fld pct; fmul [0x007A5170]=100.0f; call
   _ftol2 @0x005DE394 -- the fld/fst/fistp/fild truncation dance), so a
   6-fellow roster displays 45% where retail shows 44%, and 8 fellows
   shows 35% vs 34%. The table itself IS byte-exact; only the ->int
   conversion diverges, and it is not fixable by a plain cast because
   0.45f*100f already rounds up to 45.0f in single precision.
2. D6's client-side invite intercept has no retail anchor. Read in full:
   Handle_Character__ConfirmationRequest @0x005640A0 (bare jump table),
   RecvNotice_FellowshipRequest @0x00490880, MakeFellowRequestDialog
   @0x00490620 (only guard is m_fellowRequestContext), plus a whole-file
   sweep of both option accessors -- zero reads on any confirmation path.
   The code comment cites ACE's Fellowship.cs as "retail". ACE filters
   both bits server-side, so the intercept is dead against a correct
   server and harmful against a drifting one -- and IgnoreFellowshipRequests
   defaults to TRUE client-side.
3. D4 never re-declares 0x00A6 after a generation reset: the edge-
   triggered _pageVisible latch survives reconnect, so fellow vitals stay
   frozen for the whole new session. ResetSessionTransientUi is the seam.
4. gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 is not ported --
   selecting a fellow in the WORLD leaves Dismiss/Leader disabled and no
   row ever shows selected; the plan's contracted UiTemplateListBox
   selection model + 0x1000000D row instance-id were not added.
5. Three shipped deviations have no register row (invite intercept, gold
   leader tint, name-text-only row selection); the Recruit is-a-player
   gate's "inline comment, not a register row" call is also wrong.

Re-derived rather than trusted: the live-mount probe was re-run against
the installed DATs (every ledger element/string claim CONFIRMED, Bind()
warning-free), the GetEvenSplitXPPctg table was byte-read from the
PDB-paired binary, FlushPreservingScroll's shrink semantics were traced
through UiScrollablePanel/UiScrollable (sound), and the five touched
test classes pass 93/93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 05:01:53 +02:00
Erik
6849b45771 docs: FA4 mechanism review -- APPROVE-WITH-FIXES (1 MUST-FIX, 3 SHOULD-FIX, 2 NIT)
Mechanism-faithfulness lens on 357d2032/5bdd0528/38f08314. Every
wire-touching mechanism verifies retail-faithful: the D4 0x00A6 gate
(all five in-session transitions + idempotence + no-send-while-
disconnected), the leader-quit 0x0290-before-0x00A3 hand-off routing,
the D5 byte-exact even-split table, the D6 type-4 auto-response +
Runtime mutual exclusion, the D7 four-row un-dim (35->31 conformance),
scroll preservation across a rebuild, create-flow refusal-by-enabled-
state, and the button-enable rules. Live-mount probe PASSES 1/1 against
real DATs; FA4 suites 75/75 App + 28/28 Runtime under --no-build.

MUST-FIX: the leader-gold-tint adaptation has no divergence-register row
(AD-80/AD-81 don't cover it). SHOULD-FIX: AD-78's stale 35-of-50 count;
D4 not re-armed across a reconnect while the panel stays open; no
panel-level test pins the D4 conjunction. NITs: caption pre-toggle,
_ftol2-vs-Round (both non-blocking).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 04:59:35 +02:00
Erik
eda8729c92 docs: FA4 blast-radius review -- APPROVE-WITH-FIXES (1 SHOULD-FIX)
All nine blast axes verified clean at the code level. Single fix:
the AD-78 register row still reads "35 of 50 rows dimmed" after FA4's
D7 flipped four rows to Live (now 31 of 50) -- the class doc and
conformance test were updated, the binding register row was not.
Plus one minor non-blocking observation on GetMembers' per-vitals-tick
allocation profile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 04:57:11 +02:00
Erik
38f08314c7 docs(fa4): register rows AD-80/AD-81, AD-78 addendum, gate script section, ledger
Register: AD-80 files the D5 XP-share display divergence between
retail's byte-decoded table (acdream renders it verbatim) and the
currently-targeted ACE server's slightly different actual grant (.3 vs
.3111111 at 9 fellows, no 10-fellow row, wrong out-of-range default) --
an ACE-vs-retail gap, not an acdream-vs-retail one, filed because it is
directly user-visible through this panel. AD-81 files the two unported
retail text-composition primitives the fellowship page's mechanism
needs (StringInfo variable substitution, ACCharGenData::FormatName) and
what acdream renders instead (plain numeric composites, the raw typed
name). AD-78's derivation table gains its D7 addendum: 4 of the 35
store-only rows (IgnoreFellowshipRequests/FellowshipAutoAcceptRequests/
FellowshipShareXP/FellowshipShareLoot) moved to the Live bullet with
their new consumers named.

Gate script: new §FA4 section covering create (name + shareXP), the
open/close caption swap, button-enable rules, and the D5 display -- all
solo-testable -- plus roster/recruit/dismiss/leader-handoff/invite-
dialog steps marked [TWO-CLIENT] with an honest note that they defer to
FA6's bot-vs-ACE gate if a second account isn't available for this
connected gate. Corrects FA3's now-stale "these six buttons/four
checkboxes are INERT" claims in steps 11-12 to point at the new
section instead of leaving a wrong claim in place.

Ledger: FA4 row CODE-COMPLETE with both commit SHAs, the reconciled
13,238->13,272 (+34) test-count arithmetic, the live-DAT verification
summary (ACDREAM_PROBE_LIVE_MOUNT=1 against real installed DATs,
including the structural finding that retail's own frame-visibility
swap already gates the Create-flow controls away from the roster view
with no extra code needed), and the four scoped
deferrals/simplifications this slice made (the StringInfo/FormatName
gap, the proportional-share omission, the Recruit button's
superset-of-retail enable rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 04:41:53 +02:00
Erik
afb3223c9b docs: FA3 CODE-CLOSED (re-review bf07b70e) — carry-forwards folded into FA4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:53:07 +02:00
Erik
bf07b70ef1 docs: FA3 mechanism re-review -- CLOSED, no reopen (4 carry-forwards)
Narrow re-review of the FA3 fix round (9afa05b5/35c40a9b/ae772709/
a5553904/c5f73744) against this doc's 2 MUST-FIX and 9 SHOULD-FIX. Every
disposition re-derived from the diffs, not from the commit messages. All
11 correctly applied; nothing reopened; no new MUST-FIX or SHOULD-FIX.

Dispositions worth naming: MF-1/MF-2 were fixed as script REWRITES that
state the verified truth (Allegiance is both the default and the
left-most tab; @allegiance info prints chat data AND the blocks stay
hidden, report neither) rather than deletions, and MF-2's report-bullet
is correctly inverted to "blocks becoming VISIBLE is the anomaly".
SF-3's assertions pin more than asked (which page is visible, not just
that one is). SF-7 landed as a binding FA5 acceptance line plus the
forward hazard about Tick()'s unconditional LinesProvider reassignment,
the right altitude for a shell slice.

Claims verified rather than accepted: SF-1's no-register-row precedent
(grepped -- no row exists for any Toggle*Panel close-on-second-press, so
the precedent is real); the #383 timestamp correction (b4edee97
2026-08-11 09:19, e71e5a96 06:25, 74c3d85d 2026-08-12 02:58 = ~17h39m
and ~20h33m, previous day); OnShown/OnHidden really are driven by
RetailWindowHandle.NotifyVisibility and correctly do NOT fire for a
window mounted Visible=false; UiTemplateListBox.Scroll forces the
extent-seeded viewport so pre-row scrollbar wiring is sound; and the new
template cache is safe because Build is the pure builder -- only
BuildFromInfos (tests-only) mutates the ElementInfo it is handed.

Gates re-run on the post-fix Release binaries: live-mount probe passes
against the installed DATs with the promoted assertions showing
Allegiance Visible=True (other three False) and 0x10000492 count = 2;
AcDream.App.Tests 4,876 passed / 3 skipped / 0 failed, exactly +5 over
the pre-fix 4,871 and exactly the ledger's App figure, so the 13,238/4/0
+5 reconciles at the only project this round touched. Blast radius is 14
files, all FA3's own plus doc-only edits to UiTemplateListBox and
MountSocialPanel.

Carry-forwards (non-blocking): the Flush scroll-position reset will bite
harder in FA4's per-vitals-tick roster rebuild; the production template
cache has no test (both long-roster tests use the fake resolver); the
scrollbar element ids are literals where ScrollbarElementId carries the
authored value (cohort-wide nit); and the allRowsResolved retry rebuilds
per frame on a permanently unresolvable template (bounded and cheap).

FA3's remaining obligation is unchanged: the user's connected gate,
against a script that no longer contains two instructions guaranteed to
produce false defect reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:52:08 +02:00
Erik
c5f73744f0 docs(fa3): update plan ledger with fix-round SHAs and final totals
Records the FA3 fix-round closure: both dual-lens reviews' findings
(mechanism 2 MUST-FIX/9 SHOULD-FIX, blast 1 MUST-FIX/6 SHOULD-FIX/1 NIT)
applied across four commits (9afa05b5/35c40a9b/ae772709/a5553904).
Baseline 13,233/4/0 (13,237 total) -> fix-round 13,238/4/0 (13,242
total), +5 tests, arithmetic verified directly against all 9 test
project totals. FA3 still owes the user's connected gate against the
now-corrected script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:46:10 +02:00
Erik
a555390483 docs(fa3): fix gate script tab order + false-defect route, add scroll/restore-open steps, correct #383 timing, fix bold markers + add U11
Mechanism MUST-FIX 1: the connected-gate script's steps 1/9/10 carried
the REFUTED x-order guess forward — claiming Friends was drawn left-most
and that the authored default (Allegiance) was somehow NOT the left-most
tab. The real authored order (fixture + live-mount probe, corroborated
by each page's own P0x57 action-map id) is Allegiance (x=0, DEFAULT),
Fellowship, Friends, Squelch — the default tab IS the left-most tab.
Fixed steps 1, 9, 10, and the "What to report" bullet that repeated the
wrong claim.

Mechanism MUST-FIX 2: step 14 sent the user to `@allegiance info` as a
trigger that would supposedly reveal the monarch/patron blocks, and told
them to report it if it didn't — the trigger CANNOT fire post-FA2
(0x0020 AllegianceUpdate is the only inbound writer of this panel's
data; 0x027C, the @allegiance info response, stopped seeding it in
4272ad0e) and FA3 sends no 0x001F subscription at all (FA5 scope). The
script primed the user to file a false defect. Rewritten to state the
true FA3 expectation: @allegiance info prints real data to chat, the
panel blocks stay hidden regardless, for the whole gate — report
NEITHER half as a bug; the actual anomaly to watch for is the blocks
becoming visible at all.

Mechanism SHOULD-FIX 4: step 11's Fellowship checkbox count hedge
("three... a fourth may also be present") replaced with the settled
count (four).

Blast NIT 8 / gate note: added two steps the original script never
exercised — a long-roster Friends/Squelch scroll check (exactly where
blast MF-1's scrollbar-wiring fix bites, and a short test roster would
never surface it) and an honest restore-open-across-relaunch
observation step (the social panel follows the SAME restore-open
convention every sibling main panel already has — Options/Spellbook/
Character/Inventory/Vitae — stated up front so it isn't mistaken for a
bug mid-gate).

Blast SHOULD-FIX 6: docs/ISSUES.md #383 said the two drifted fixtures
were committed "days ago" — git says otherwise: ~18h and ~21h before the
FA3 regeneration run, the previous day. Corrected, and added the
mechanism reviewer's no-drift finding for the NEW social-panel fixture
(cross-checked against the live probe on every axis, zero drift) —
narrows the issue to exactly the two pre-existing OP-era fixtures.

Blast SHOULD-FIX 7: the §10 addendum in fa-panel-structure.md had five
`**` bold markers (odd count) — an orphaned trailing marker bled bold
formatting into the following section. Dropped the orphan; the addendum
now bolds only its lead sentence and the inline "Allegiance" callout,
both balanced pairs.

Mechanism SHOULD-FIX 1 (research-doc half): filed unknown U11 in §8 —
what a repeat F3/F4 press does when the panel is open on the OTHER tab
is not established from retail decomp (no OnAction consumer exists for
either action in the binary); acdream's own OpenSpellbook-precedent
choice is not a retail port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:44:26 +02:00
Erik
35c40a9b56 fix(ui): allegiance page — hoist per-frame LinesProvider allocation, document the coarser empty-state gate
Mechanism SHOULD-FIX 2 / blast SHOULD-FIX 4 (same finding, both reviews):
SocialAllegiancePageController.Tick() closed over a local `lines` on
every call — `() => lines` allocated a display class plus a delegate on
EVERY frame, unconditionally, at ~2,000 allocations/second at the
profile's measured FPS, in a hot loop the Modern Runtime slices spent
whole commits driving to 0 B/frame. Hoisted two static readonly
Func<IReadOnlyList<UiText.Line>> providers (BlankLineProvider/
NoLinesProvider); Tick() now assigns the cached delegate reference —
zero allocation while idle or active.

Mechanism SHOULD-FIX 7: FA3's empty-state gate is coarser than the
retail mechanism it is contracted against — gmAllegianceUI::UpdateMonarchData
@0x00491B40 hides the monarch/patron blocks per-relationship (monarch
block also hides when the monarch IS the viewer; patron block on the
analogous test), while this shell gates both blocks on the single
HasProfile flag. Not a MUST-FIX for FA3 (MF-2 in the same review means
HasProfile is effectively always false for the whole FA3 gate, so
nothing wrong is visible during this slice's own gate) — recorded
instead as an explicit FA5 acceptance line in the plan's FA5 row so the
gap cannot be silently lost, plus a class-doc note that FA5's real
monarch/patron population must also change Tick()'s unconditional
LinesProvider reassignment in the same commit or its content will be
overwritten the next frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:43:48 +02:00
Erik
9e622f565e docs: FA3 mechanism review -- APPROVE-WITH-FIXES (2 MUST-FIX, 9 SHOULD-FIX)
Campaign FA slice FA3 mechanism-faithfulness review of 0a9ca2f1 /
74c3d85d / b6a25110 / d7e1cffd.

The load-bearing tab-table correction VERIFIED CORRECT three independent
ways: the authored 0x2E array re-parsed straight out of the committed
fixture (Allegiance 0x1000028C -> 0x10000291 is the sole IsDefault entry),
each button's caption resolved from the installed DATs via the live-mount
probe, and each page's own P0x57 + RegisterElementClass type re-read in
the pseudo-C. A fourth corroboration the addendum missed: under the
corrected pairing the default tab is also the LEFT-MOST tab (x=0).

MUST-FIX (both in the user's connected-gate script, neither code):
1. The script still carries the REFUTED x-order -- step 9 states the strip
   as "Friends, Allegiance, Fellowship, Squelch" and step 1 primes the user
   to expect Friends left-most. Real geometry: Allegiance x=0, Fellowship
   x=72, Friends x=144, Squelch x=206.
2. Step 14 tells the user `@allegiance info` should reveal the
   monarch/patron blocks and to "report if they do not" -- FA2's own fix
   round deliberately stopped 0x027C from seeding RuntimeAllegianceState,
   ApplyUpdate (0x0020) is the only writer of _hasProfile, and FA3 sends
   no 0x001F. The script steers the user into a false defect report.

SHOULD-FIX: unsupported "retail's Toggle-action semantics" claim on F3/F4
(no P0x57 read site, no OnAction handler, folded gmPanelUI global-message
stub -- the rule is acdream's OpenSpellbook precedent, not retail);
per-frame closure allocation in SocialAllegiancePageController.Tick; two
probe findings printed but never asserted (0x10000492 count, page
exclusivity); "three checkboxes" is four; AD-79 enumerates seven controls
but its cited test pins six; the two page controllers do not name AD-79;
the allegiance empty state is gated on HasProfile rather than retail's
per-relationship rule (TryGetMonarch/TryGetPatron already exist);
Tick() ignores _disposed; FindDeepest's doc overstates its guarantee.

Verified clean: every §6 Campaign-OP lesson (string resolver on both
Builds, scoped lookups -- I enumerated ALL duplicate ids and found three
previously-uncalled-out cross-page repeats, tab activation, no 0x0-extent
lazy children, cross-layout templates so the same-layout skip cannot
apply, no hand-rolled viewport); U6 genuinely closed (page 0x10000292 has
exactly two children); Flush/ClearContent resets ContentHeight; the J4.1
owners are borrowed by reference and clear in place; catalog id 12
byte-verified and the toolbar seam tolerates it via the same path four
existing non-toolbar panels take; window-frame policy byte-identical to
Options. data_794358 BYTE-VERIFIED in the PDB-paired binary as UTF-16LE
" " (one space, not empty) -- lane A's L" " reading and FA3's BlankLine
both correct. Live-mount probe passes against the installed DATs with no
fixture drift; 22 FA3 tests and 4,871 App tests / 3 skips / 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:23:16 +02:00
Erik
e9eb756480 docs: FA3 blast-radius review -- APPROVE-WITH-FIXES (1 MUST-FIX, 6 SHOULD-FIX)
MUST-FIX 1: the Friends/Squelch lists have NO scroll driver. Both
controllers set TemplateResolver but never wire the authored scrollbar's
Model (0x10000518 / 0x10000543, both direct siblings of their ListBox
under the page root per FA3's own fixture), unlike all four existing
UiTemplateListBox consumers. There is no wheel fallback -- wheel scroll
lives only on UiText, not UiScrollablePanel -- so Scroll has no driver at
all. Visible at authored size: the 400/430-tall ListBoxes sit at y=40 in
a 362-tall panel, so rows past ~322px are off-panel AND unreachable.
AD-79 covers the inert BUTTONS, not a dead scrollbar.

SHOULD-FIX: the revision-driven rebuild does N live DAT imports under the
shared DatLock while the panel is CLOSED (first repeating consumer of a
resolver every other caller invokes once at Bind); Refresh() consumes the
revision before building so one resolver miss latches an empty list;
per-frame closure allocation in the hidden allegiance page; Flush()'s doc
omits its scroll reset and diverges from the sibling UiItemList.Flush()
it shares a name with; #383's "days ago" is really ~18-21h per git;
the SS10 addendum's trailing ** is orphaned.

Verified clean: catalog/window-name consumers all degrade safely
(SetPanelOpen(12) is a no-op, opacity controller correctly scoped by
#379); persistence omission is the documented cohort behavior; F3/F4
plumbing predates FA3 entirely (UI.Abstractions untouched) and the
apparent bare-F3 duplicate is in the non-production AcdreamCurrentDefaults;
mount order respects the DialogFactory constraint; SocialRuntimeBindings
is required-positional with one construction site; the generator is
env-gated and only the new fixture landed; +18 reconciles exactly
(23 targeted + 289 blast-radius regression tests pass on the FA3 binary);
AD-79 well-formed with the count bumped 58->59; and the corrected tab
table was independently re-read from the fixture's own TabTable
(0x1000028C -> 0x10000291, IsDefault=true -- Allegiance IS the default).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:14:42 +02:00
Erik
3e6dc62b0e docs: file #383 — installed-DAT vs committed-fixture drift found at FA3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:01:36 +02:00