Commit graph

3814 commits

Author SHA1 Message Date
Erik
0baebce262 fix(ui,runtime): Campaign LA gate-round-2 batch-review fixes F1,F3-F8; file #401
F1 (MUST-FIX): RetailWaitDialogView was the ONE dialog view the 0a7dc7d6
EffectiveCanvasSize sweep missed - the Entering World wait dialog (fires
on ENTER, the char screen primary action) still centered against the raw
window and landed off the visible canvas. Same three-line fix as its three
siblings; the enter-wait test now grows the window over the fixed canvas
and asserts canvas-space centering.

F3/F4: two stale assertions about the DELETED first AD-98 substitution
(the register section-2 header line and the live-DAT oracle test doc) now
describe the completed FixedCanvasSize mechanism - the C4-closeout failure
mode, caught before it cost anything.

F5: RetailDialogData.Confirmation sets ElementAttribute40 itself (retail
MakeConfirmExitDialog writes 0x8E=1, 0xAC=1, 0xC5); the manual set in
GameplayConfirmationController is gone.

F6: MapWindowToCanvas truncates instead of rounding - rounding mapped the
window far edge one past the canvas last valid coordinate, a 1px dead
hit-test band; test updated to truncation semantics + far-edge case.

F7: AD-98 records that the no-letterbox aspect claim has no decomp
citation and is confirmed by the user live gate pass 2026-08-15.

F8: the durable world-name read in StartCore is IsCurrent-gated like every
neighbouring step.

F2 filed as #401 (invert RetailUi to opt-out - product-default decision,
not a gate fix).

App 5100+6 skips, Runtime 1666, green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:28:06 +02:00
Erik
36c14902a8 docs: Campaign LA gate round 2 — char-select matrix USER-PASSED
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:07:04 +02:00
Erik
0a7dc7d626 fix(runtime,ui): Campaign LA gate round 2 — world name reads durably; dialogs center on the canvas
Two live-integration gaps the ef96c554 unit tests could not see:

1. World box stayed empty against ACE: ServerName (0xF7E1) arrives in the
   SAME connect batch as CharacterList, so ServerNameReceived fires during
   the handshake pump BEFORE the controller binding subscribes - the
   event-only wiring proved the state and controller but never the live
   ordering. StartCore now reads the durable WorldSession.ServerInfo after
   connect exactly like the roster (ILiveSessionOperations.GetServerInfo,
   default interface method so no fake breaks); the event remains for
   post-connect updates. Pinned by a Start-level test.

2. The exit confirmation rendered far right of the screen: all three
   retail dialog views centered against the raw window size while the
   active screen lays out in the fixed 800x600 canvas - center-of-1920
   is canvas-760, which the stretch pushes off-center. Views now center
   against UiRoot.EffectiveCanvasSize (canvas while a pre-world screen is
   active, window otherwise). Pinned by growing the window over the fixed
   canvas in the exit-dialog test and asserting the scrim spans the canvas
   with the popup centered at 400.

Runtime 1666, App 5100+6 skips, green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:02:33 +02:00
Erik
2e6d69ddc7 docs: file #400 (Credits -> gmCreditsUI, post-LA) and fix the misfiled comment
The ef96c554 batch ghosted Credits with a comment claiming it was filed
as #397 - that number is the Windows graceful-stop issue and no Credits
entry existed. #400 now records the gap properly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:51:57 +02:00
Erik
ef96c55489 fix(ui,net): Campaign LA gate round 2 — char-select exit confirmation, authored row justify, world name
Finding 1 (Exit button dead): retail's gmCharacterManagementUI Exit
button (element 0x100003A4, offset 7 from the listbox base in
ListenToElementMessage@0x004ed5a0) opens MakeConfirmExitDialog
(0x004ed250), whose exact ID_CharacterManagement_ConfirmExit text
(table 0x23000002) and m_confirmExitDialogContext re-entry guard are
now ported. On confirm (matching RecvNotice_CloseDialog@0x004ed760
case 1's ConfirmationResult check) the client exits through the
EXISTING graceful window-close seam (CharacterSelectionRuntimeBindings
.RequestExit -> d.Window.Close, the same delegate
GameplayInputCommandController's Escape fallback already uses) so
disconnected/exited status events still fire via GameWindow.OnClosing
-> CompleteShutdown. Retail's real post-confirm destination is
QueueUIMode(0x10000009) -> gmEpilogueUI, an epilogue screen this round
does not port — recorded as AD-99. Credits (element 0x100003A3,
QueueUIMode(0x10000005) -> gmCreditsUI) stays visibly ghosted like
Create, same treatment, out of scope this round.

Finding 2 (row names center-aligned, retail is left): the character
row template (LayoutDesc 0x21000004, element 0x100003A5, live-DAT
confirmed HJustify=Left with three stateful Type-3 highlight-art
children and no Type-12 caption child) authors its OWN justify
directly, with no separate text child to lift a label from.
DatWidgetFactory.BuildButton's Left-justify branch required
!ReferenceEquals(labelInfo, info) — true only when a label was LIFTED
from a distinct child — so a button's own direct HJustify=Left was
silently dropped to UiButton's Center default. Widened the branch to
also honor the direct case, preserving the existing lifted-child
LabelOffsetX behavior and leaving genuinely-centered buttons
(CREATE/ENTER/DELETE/RESTORE) untouched.

Finding 3 (World box empty): parsed ACE's GameMessageServerName
(opcode 0xF7E1, ACE.Server/Network/GameMessages/Messages/
GameMessageServerName.cs; retail CM_Login::DispatchUI_WorldInfo
@0x006ad860 -> ClientUISystem::Handle_Login__WorldInfo@0x005641a0 ->
ECM_Login::SendNotice_WorldName@0x00692b10, notice 0x186a2, consumed
by gmCharacterManagementUI::UpdateWorldName@0x004ec120 /
RecvNotice_WorldName@0x004ec360 onto element 0x1000039B) as
src/AcDream.Core.Net/Messages/ServerName.cs, cross-checked against
holtburger's ServerNameData. WorldSession.ServerNameReceived fires
alongside CharacterListReceived (ACE sends both in one
SendConnectResponse batch); RuntimeCharacterSelectionState.
ApplyWorldName is the new J-owner field (ungated by lifecycle, since
either message can arrive first); CharacterManagementUiController
binds it onto the WorldTextElementId UiText. Per the LA1 status
vocabulary, the characterList STATUS event's worldName field is
intentionally NOT added this round (kept bounded to the client-side
fix) — a follow-up if the launcher UI wants it.

Also corrects AD-44, discovered stale while filing AD-99: its opening
claim ("acdream has no retained character-management screen") was
false as of this session — LA7/LA8 shipped the screen in earlier
commits without updating this row.

Tests: exit-confirm open/cancel/confirm/re-entry-guard flow;
DatWidgetFactory own-HJustify-Left/Center regression tests plus the
live-DAT pinned row-justify assertion; ServerName parse round-trip
(byte-exact vs ACE's AceWireWriter fixture, truncation/wrong-opcode
cases); WorldSession dispatch test (roster+world in one wire batch);
RuntimeCharacterSelectionState.ApplyWorldName tests (order-independent
of ApplyRoster, unchanged-value no-op, Reset clears); controller test
binding the World text element to the live snapshot. Extended the
shared RetailDialogFactoryTests.BuildDialogLayout test fixture with a
Confirmation-type branch (Accept/Reject buttons) since this is its
first RetailDialogType.Confirmation consumer.

Suites: full solution Release build green; AcDream.App.Tests 5100/6
skips, AcDream.Core.Net.Tests 965/0, AcDream.Runtime.Tests 1665/0, all
Release, 0 failures; live-DAT probes (ACDREAM_PROBE_LIVE_MOUNT=1)
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:50:45 +02:00
Erik
308f40a3fb fix(ui): Campaign LA gate round 2 — fixed-canvas stretch filters bilinearly like retail's presentation blit
AD-98's fixed-canvas stretch (73041d70) scales every retained-UI quad at
TextRenderer.AppendQuad, but the live gate reported it JAGGED — text
especially. Cause: dat-font glyph atlases and IconComposer's composited
icons upload nearest (TextureCache.UploadUiTexture's UiNearestRepeat
sampler) — correct at the native 1:1 scale (pixel-exact retail art), but
aliased once magnified 2.4x1.8. Chrome/background art was already fine:
it uploads through GpuSamplerDescription.WorldRepeat (linear) by default.
Retail's own fixed-canvas presentation is a single bilinear-filtered
frame blit, never a per-texture stretch — this closes that gap one step
earlier, at the source texture, without adding RHI surface area.

- TextureCache.GetOrCreateLinearUiTwin: lazily registers a SECOND table
  slot for a nearest handle's IGpuTexture, sampled WorldRepeat (linear)
  instead of nearest — no re-decode, no re-upload, no extra memory-ledger
  bytes. Returns the handle unchanged for anything never registered
  nearest (chrome, UiTextureTableHandle.None), so it's a cheap
  unconditional probe. Twin slots are released in Dispose without
  double-disposing the shared texture.
- TextRenderer.LinearTwinResolver + the DrawSprite chokepoint: swaps a
  sprite's texture handle through the resolver only while
  CanvasScale != One. At CanvasScale == One the resolver is never even
  called — zero overhead on the ordinary in-world/UI path.
- InteractionRetainedUiComposition wires the resolver to TextureCache
  right after every UiHost acquisition (the lease can hand back a host
  from a prior session against a fresh TextureCache).
- AD-98's register row gets one added sentence recording the fix.

Tests: TextRendererLinearTwinTests pins the renderer-side handle-swap
seam GPU-free (segment handle selection); TextureCacheLinearTwinTests
pins twin creation/reuse/dispose against RecordingGpuDevice. App suite
5097/3 skips (Release, ACDREAM_PROBE_LIVE_MOUNT=1 live-DAT probes
included). Full solution builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:05:59 +02:00
Erik
73041d7015 fix(ui): Campaign LA gate round 2 — character-select scales as one authored canvas
Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.

Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:

- UiRoot.FixedCanvasSize: while the char-select screen is active, the
  retained tree lays out in its authored canvas and Draw scopes a uniform
  scale onto TextRenderer.CanvasScale; the mouse entry points apply the
  exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
  rects, AND glyphs scale together, including retail-authentic non-uniform
  aspect distortion and stretched text. World-space HUD stays native (the
  scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
  activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
  StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.

Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 10:39:19 +02:00
Erik
71bf24fb6f fix(ui): Campaign LA gate round 2 — character-select root background stretches, never tiles
The LA8 char-select root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=
BottomEdge=0 ("no anchor") in the installed DAT — confirmed via the new
CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf
gate — so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never
resizes this element; it stays a fixed 800x600 rect in retail's own tree.
Retail's generic UI sprite blit, Graphic::Draw (0x00693b20) dispatching to
Graphic::PutImage (0x00693a30) for an exact/undersized destination or a
modulo-wrapped tile loop otherwise, has no third "stretch" mode — confirmed
against BlitMode (acclient.h ~3135) and MD_Data_Image::m_drawMode/DrawModeType,
both COLOR-blend selectors, not tile-vs-stretch geometry modes. The prior
"Normal -> tile, matching ImgTex::TileCSI" citation in UiDatElement was a
mis-attribution: ImgTex::TileCSI (0x0053e740) is called exclusively from
TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain texture
compositing, never from the UI element system.

Given the dat authors zero resize anchors and the blitter can only copy or
tile, the only way retail's whole pre-world scene (background + buttons +
listbox together) fills an arbitrary window resolution is that these
fixed-canvas "flow" screens render at 800x600 and the WHOLE FRAME is
stretched once at presentation — outside the UI sprite system entirely.
acdream has no offscreen fixed-resolution UI render target / present-time
scale pass; CharacterManagementUiController's constructor instead resizes
the MOUNTED ROOT element itself to the live viewport, which is why its own
background tiled (Width/tw > 1 at any resolution above 800x600, wrapped by
GL_REPEAT).

Fix: UiDatElement gains StretchOwnBackgroundToFill (default false, every
ordinary chrome/container element keeps tiling) — when set, the element's
own DirectState background draws as one UV-0..1 quad instead of the native
tile formula. CharacterManagementUiController sets it on Root right where
Root is resized to the host viewport, reaching the same visual result as
retail's present-time stretch (no tiling, no aspect-preserving letterbox)
through a different mechanism. Divergence register row AD-98 records the
substitution.

Tests: three new UiDatElementTests pin the UV-span mechanism generically
(tile past 1.0 when unset and rect exceeds native size; clamped to 1.0 when
set; byte-identical to the old tile formula when rect equals native size,
so every unaffected panel is untouched). CharacterManagementUiControllerTests
pins Root.StretchOwnBackgroundToFill == true post-construction. The live-DAT
gate confirms the root's zero edge-anchors and Type=3 against the installed
DAT. AcDream.App.Tests: 5084 passed / 3 skipped with ACDREAM_PROBE_LIVE_MOUNT=1
(5081/6 skipped without it — the 3 live-DAT-gated tests skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:42:53 +02:00
Erik
936077d576 docs: Campaign LA gate round 2 record — retail-UI product default + JPEG background decode
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:13:45 +02:00
Erik
9ce7292570 fix(ui): Campaign LA gate round 2 — character-select screen media resolution
Root cause: the LA8 character-select screen's root background RenderSurface
(0x06007576, LayoutDesc 0x21000004 element 0x1000039A) is PFID_CUSTOM_RAW_JPEG
— a complete JFIF byte stream (confirmed live: 414,230 bytes, FFD8...FFD9,
Width=0/Height=0 on disk) that SurfaceDecoder.DecodeRenderSurface had no case
for, so it fell through the switch's `_ => DecodedTexture.Magenta` default arm
with nothing logged. Retail's RenderSurface::CreateFromSourceData
(named-retail decomp @0x004440a0) hands this exact byte stream to the Intel
JPEG Library (`_ijlInit`/`_ijlRead`/`_ijlFree`) at runtime and reads the real
pixel dimensions from the JPEG's own SOF header rather than this
RenderSurface's Width/Height fields, which are legitimately 0 for this
format — the same reason the decoder's generic non-positive-Width/Height
guard was also wrong to apply here.

A per-id media sweep of the installed DAT (new EveryDeclaredMediaId_
ResolvesToADecodableTexture test) showed this was the ONLY unresolved id
among the screen's 25 distinct media ids — the listbox (0x1000039D) and every
button face resolve fine. The listbox interior and the ENTER button's
circular fill are both transparent regions layered on top of the root, so
the one broken root background bled through everywhere nothing opaque
covered it, producing all three symptoms (full-screen background, listbox
interior, ENTER circle) from one cause.

Fix: SurfaceDecoder now special-cases PFID_CUSTOM_RAW_JPEG before the
Width/Height guard and decodes it with StbImageSharp (dual Unlicense/MIT,
pure managed, no native dependency — works on the Linux headless/graphical
targets Slice K/L commit to). JPEG is ITU T.81-standardized, so any
conforming decoder reproduces the pixels IJL would; round-tripped a
synthetic fixture through the real decode path to confirm. Verified against
the live DAT: 0x06007576 now decodes to 800x600, exactly the screen's
LayoutDesc-authored size.

Guard: per claude-memory/feedback_ui_resolve_zero_magenta.md, an unresolved
id reaching the draw path should be loud. That memory's existing guard
("guard on the id, not the handle") only covers a DIFFERENT trap — a
zero/absent id — and could not have caught this one, which has a real,
non-zero, DAT-resolved id. No guard existed for "id resolves but can't
decode" or "id doesn't exist in either dat" before this change, so both were
silent. SurfaceDecoder now logs once per surface id on every magenta-return
path (null data, JPEG decode failure, unsupported format, no-palette
paletted format, decode exception); TextureCache.GetOrUploadRenderSurface
logs once per id when a RenderSurface isn't found in Portal or HighRes at
all.

Tests: CharacterManagementLiveDatTests.EveryDeclaredMediaId_
ResolvesToADecodableTexture (installed-DAT gate, ACDREAM_PROBE_LIVE_MOUNT=1)
sweeps every StateMedia id in the char-select root + listbox row template
and asserts none decode to the magenta placeholder — this class of gap now
fails the gate instead of shipping silently. SurfaceDecoderTests adds
PFID_CUSTOM_RAW_JPEG coverage (real decode via a synthetic from-scratch
JPEG fixture — not retail art, generated with StbImageWriteSharp and
round-tripped before being pasted in as a literal; corrupt-data and
null-SourceData magenta paths) plus PFID_P8/PFID_INDEX16 no-palette cases
that now flow through the same logged path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:12:10 +02:00
Erik
6e1c0967cb fix(app): Campaign LA gate round 2 — session-config launches force the retail UI on
A launcher-spawned client showed the world with NO interface at all -
character screen included. RetailUi rode ACDREAM_RETAIL_UI (the dev-era
opt-in), FromSessionConfig inherited the env parse, and the launcher
strips ACDREAM_* from children by design, so every product launch got
the dev default. A session-config launch IS a product launch: RetailUi
is now forced true on that path; the env flag remains the dev-launch
opt-in. Pinned by the session-config options test with a null env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 08:42:33 +02:00
Erik
981e168fb9 fix(launcher): Campaign LA gate-round-1 review findings F1-F6 + hardening
F1: the crash reporter comment claimed the launcher never holds a password
in any field - false (ProfileEditorDialogViewModel, AccountProfile.Password,
StartRequest.Password). Reworded to the true, narrower invariant (no throw
site interpolates a credential VALUE into an exception message) and pinned
it with CrashReportNeverContainsAStoredPassword: a real STJ failure over a
profiles document containing a known password, corrupted after the
credential, must yield a crash file with the stack and without the value.

F2: the co-deploy Inputs covered only Bake own sources; a Content edit
never refreshed the 83 MB exe. Now the full reference closure. Fixing it
surfaced two more incrementality traps, both fixed and comment-documented:
SkipUnchangedFiles left the output older than the triggering input (target
re-ran forever - added an explicit Touch), and %(Item.Metadata) in a plain
Include does not batch (the literal percent-text became a permanently
out-of-date phantom input - globs are now spelled per project). Verified:
Core edit retriggers, then two consecutive clean incremental builds.

F3: RID publishes ran BOTH co-deploy paths (two self-contained bake
publishes). Build-time target now guarded on _IsPublishing; verified a
real win-x64 publish runs zero build-target co-deploys and still ships
both exes.

F4: comment misattributed PublishBakeTool=false to CI lanes; it is
target-local recursion guarding. F5: the x:Name reflection sweep now walks
the markup as XML and tolerates template-scoped names (no generated field
exists for those). F6: dead using removed. Hardening: the crash reporter
positional --data-dir fallback requires a fully-qualified path so a
relative or flag-shaped value cannot create ./crash-reports at an
arbitrary CWD.

Launcher 67/67, Launcher.Core 317/317.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 08:19:23 +02:00
Erik
1f87acf1af docs: Campaign LA gate round 1 record — #398 closed, #399 merged-closed, bake co-deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:58:41 +02:00
Erik
fb64af8b25 merge: Campaign LA gate round — headless MainWindow view tests close #399
Falsification-proven: 12/12 fail against AvaloniaXamlLoader.Load (the #398
crash shape), 12/12 pass against InitializeComponent. Launcher tests 66/66
on Windows and native Ubuntu, no display required. xunit -> xunit.v3 in the
launcher test project (required by Avalonia.Headless.XUnit 12.1.1 net10.0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:55:55 +02:00
Erik
2b439cc107 test(launcher): Campaign LA — headless MainWindow view tests close #399
#398 was a crash on every modal open/close caused by MainWindow's
constructor calling AvaloniaXamlLoader.Load(this) instead of the
generated InitializeComponent() — only InitializeComponent assigns the
x:Name backing fields, so every named control was null and the first
Dispatcher.UIThread.Post callback in OnViewModelPropertyChanged threw
NullReferenceException, killing the process. It reached the user gate
because no test in tests/AcDream.Launcher.Tests (ViewModel-only) ever
constructed a MainWindow. #399 is the process gap that let that class of
defect through 14,012 green tests.

Adds Avalonia.Headless.XUnit 12.1.1 to the launcher test project. Its
net10.0 dependency group targets xunit v3, so the project migrates
xunit 2.9.3 -> xunit.v3 3.2.2 (drop-in: all 54 pre-existing tests compile
and pass unchanged under dotnet test via xunit.runner.visualstudio 3.1.4,
which already supported v1/v2/v3; two call sites needed
TestContext.Current.CancellationToken per the new xUnit1051 analyzer).
TestAppBuilder.cs wires [assembly: AvaloniaTestApplication] to a headless
AppBuilder.Configure<App>() so the real App.axaml FluentTheme is live in
tests.

MainWindowViewTests.cs adds 12 [AvaloniaFact]/[AvaloniaTheory] tests:
- an explicit non-null + type check of every x:Name field the
  code-behind dereferences (ProfilesTree, ServerNameTextBox,
  AccountNameTextBox, CharacterNameTextBox, EditorSubmitButton,
  FirstRunDatDirectoryTextBox, FirstRunCloseButton, UpdateCloseButton)
- a reflection sweep over every x:Name found in MainWindow.axaml, so a
  future named control without a matching non-null field fails loudly
- one open+close round trip per ProfileEditorKind (all seven, including
  Remove), plus the first-run wizard and the update prompt, each pumping
  Dispatcher.UIThread.RunJobs() so the queued focus callback actually
  executes instead of just being asserted vacuously
- a dedicated test for the _focusBeforeModal-restore branch (not just
  the ProfilesTree.Focus() fallback), anchored on a real focusable
  button since ProfilesTree (TreeView) has Focusable="False" under
  FluentTheme — its own tab stops are TreeViewItem rows, so the
  close-path assertions check "no exception escaped the dispatcher"
  rather than "focus landed on ProfilesTree"

Falsification (required evidence): reverting MainWindow's constructor to
AvaloniaXamlLoader.Load(this) and rerunning gives 12 failed / 0 passed —
10 tests throw NullReferenceException at MainWindow.FocusActiveModal,
propagating cleanly out of Dispatcher.UIThread.RunJobs() (confirming
dispatcher exceptions are not silently swallowed), and the 2 reflection
tests fail on an explicit "x:Name 'ProfilesTree' was null after
construction" message. Restoring InitializeComponent() gives 12 passed /
0 failed. Full launcher suite: 66 passed / 0 failed, reproduced on both
Windows and native Ubuntu (WSL, no display/Xvfb — Avalonia.Headless needs
none). AcDream.Launcher.Core.Tests: 317/317 unaffected.

No CI workflow change needed: .github/workflows/headless-portability.yml's
portable-launcher job already runs dotnet test on the launcher test
project on both windows-latest and ubuntu-latest with no display setup,
which is sufficient for Avalonia.Headless.

Closes #399.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:54:49 +02:00
Erik
e1e94697b2 fix(launcher): co-deploy acdream-bake on Build and write a crash report (#398)
Two gaps found launching the launcher for the LA11 gate.

1. acdream-bake was co-deployed only AfterTargets=Publish with a RID, so a
   plain `dotnet build` left the launcher with no bake tool beside it while
   App.OnFrameworkInitializationCompleted resolves it at
   AppContext.BaseDirectory/acdream-bake[.exe]. A developer-built launcher
   therefore reached the first-run wizard with an installer it could never
   run. CoDeployBakeToolToBuildOutput does for Build what the publish target
   does for Publish: still NO Launcher -> Bake project reference, still a
   self-contained single file so exactly one file lands beside the launcher
   rather than scattering Content/Chorizite assemblies into its output.
   Staged through obj/ because publishing straight into the launcher output
   makes the inner publish delete what the outer build just wrote.
   Inputs/Outputs keep it incremental - verified: 79.6 MB bake exe present,
   --help exits 0, and a second build skips the republish in ~1 s.

2. #398: the top-level guard printed only ex.Message, so the crash that
   preceded this commit surfaced with no file, line, or frame. The full
   exception now goes to a crash-reports file under the resolved data root
   and stderr names the path. The first implementation wrote to the machine
   real data root when option parsing itself failed, which broke LA11 process
   local roots during an isolated run; the reporter now reads --data-dir
   positionally for that fallback. Verified: report lands inside the isolated
   root and the real root stays empty.

The redaction comment states exactly what is guaranteed - args/environment
are never serialized, while exception text may quote an option name or path,
which is safe only because credentials never enter launcher state.

Launcher.Core 317/317 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:42:29 +02:00
Erik
ef9d610459 docs(issues): file #398 and #399 from the launcher gate launch
#399 (HIGH, process class): no test constructs MainWindow — the launcher
test project is ViewModel-only with no Avalonia headless package, which is
how a crash on every modal open/close passed 14,012 green tests and reached
the user gate. Fix direction is Avalonia.Headless.XUnit plus a view test
that drives every modal open/close, catching the class rather than one
spelling.

#398 (MODERATE): the top-level guard prints only ex.Message, so the fatal
NullReferenceException fixed at d54b8a78 surfaced with no file, line, or
frame; diagnosis needed a temporary code edit and rebuild. Fix direction is
a redaction-scanned crash file under the data root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:32:06 +02:00
Erik
d54b8a789e fix(launcher): Campaign LA — MainWindow must call InitializeComponent, not AvaloniaXamlLoader.Load
Every click in the launcher exited the process. MainWindow ctor called
AvaloniaXamlLoader.Load(this), which loads the XAML tree but never assigns
the generated x:Name backing fields, so ProfilesTree, ServerNameTextBox,
AccountNameTextBox, CharacterNameTextBox, EditorSubmitButton,
FirstRunDatDirectoryTextBox and UpdateCloseButton were all null. Opening or
closing any modal calls Focus() on one of them via
OnViewModelPropertyChanged, so the NullReferenceException escaped the
dispatcher and Program's top-level guard exited 74. A fresh isolated-root
start auto-opens the first-run wizard and hit the same line with no click
at all.

App.axaml.cs keeps AvaloniaXamlLoader.Load - that is the correct idiom for
Application.Initialize(), which has no named controls.

Verified live: the isolated-root launch that died instantly now stays up
with the first-run wizard open and the window responding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:31:34 +02:00
Erik
c25545e8d6 docs(launcher): prepare Campaign LA user gate 2026-08-15 02:16:41 +02:00
Erik
a22f54117b docs(launcher): record Campaign LA11 automated closeout 2026-08-15 02:08:18 +02:00
Erik
d39f3098d5 merge: Campaign LA LA11 - automated closeout review-closed
# Conflicts:
#	docs/plans/2026-08-14-launcher-campaign.md
2026-08-15 02:07:39 +02:00
Erik
9f9c116792 fix(launcher): harden LA11 gate evidence 2026-08-15 01:45:15 +02:00
Erik
accd01a008 fix(launcher): harden Campaign LA11 gate evidence 2026-08-15 01:08:41 +02:00
Erik
134edabed2 feat(launcher): complete Campaign LA11 pre-gate support 2026-08-15 00:02:04 +02:00
Erik
0ac8bf6171 docs(launcher): close Campaign LA10 2026-08-14 23:50:23 +02:00
Erik
da4fb3de19 merge: Campaign LA LA10 - updater review-closed 2026-08-14 23:47:25 +02:00
Erik
f881e5b467 feat(launcher): prepare Campaign LA11 user gate 2026-08-14 23:42:30 +02:00
Erik
09d84387a8 fix(launcher): verify self-update rollback sources 2026-08-14 23:41:55 +02:00
Erik
1955ca8ab5 fix(launcher): harden updater crash recovery 2026-08-14 23:12:15 +02:00
Erik
2d2a5b5046 feat(launcher): implement verified atomic updates 2026-08-14 22:09:34 +02:00
Erik
df824f366c docs(launcher): close Campaign LA6 LA8 and LA9 2026-08-14 21:18:05 +02:00
Erik
fe63ce186a merge: Campaign LA LA8 - retail character screen review-closed 2026-08-14 21:16:25 +02:00
Erik
1dd5706e15 Make character UI retries transactional 2026-08-14 21:14:08 +02:00
Erik
2198a0cc8e merge: Campaign LA LA9 - verified installer review-closed 2026-08-14 21:06:26 +02:00
Erik
208a70ac83 fix(launcher): guard orphan bake publication 2026-08-14 21:02:27 +02:00
Erik
aeac874dab Harden retail character selection recovery 2026-08-14 20:59:10 +02:00
Erik
2bb8ccb6b6 merge: Campaign LA LA6 - login commands review-closed 2026-08-14 20:48:42 +02:00
Erik
259f0e5ac3 fix(headless): route wire-only chat commands 2026-08-14 20:47:06 +02:00
Erik
3f68895120 fix(launcher): harden installer transactions 2026-08-14 20:36:11 +02:00
Erik
6cfab727f1 Implement retail character management screen 2026-08-14 20:29:19 +02:00
Erik
41b15efd4d feat(runtime): share chat commands and run login sequence 2026-08-14 20:27:45 +02:00
Erik
ff6ebb6a6a feat(launcher): add verified first-run installer 2026-08-14 20:06:37 +02:00
Erik
267804465e docs(launcher): close Campaign LA4 LA5 and LA7 2026-08-14 19:36:00 +02:00
Erik
5535d0adac merge: Campaign LA LA5 - plugin hosting review-closed 2026-08-14 19:33:27 +02:00
Erik
f820eb258d fix(plugins): close LA5 ownership races 2026-08-14 19:28:14 +02:00
Erik
60f627998c merge: Campaign LA LA4 - Avalonia launcher review-closed 2026-08-14 19:14:25 +02:00
Erik
ae2cbbee8c fix(launcher): require executable Linux hosts 2026-08-14 19:12:32 +02:00
Erik
fbe9c8a288 fix(plugins): close LA5 host lifecycle review 2026-08-14 19:05:13 +02:00
Erik
7691cf75e2 merge: Campaign LA LA7b - selection state and flow review-closed 2026-08-14 19:05:03 +02:00
Erik
ff40656293 fix(runtime): isolate Campaign LA7b delete state 2026-08-14 19:03:43 +02:00