fix(app,headless): Campaign CC slice CC4 re-review round — R1 arbiter + R2-R4

CC4 re-review returned NOT CLOSED: R1 (MEDIUM, blocking) is a new residual
the F1 fix itself introduced, plus three LOW riders (R2, R3, R4).

R1 — nulling UiRoot.FixedCanvasSize on chargen Close() stripped it from
character-management, which stays active underneath and only sets the
canvas on its own activation edge. Root cause (reviewer-named): two
controllers writing one host-global with no owner. Fixed with the
root-cause shape (reviewer's option (c)): UiRoot.DeclareFixedCanvas(owner,
size)/RevokeFixedCanvas(owner), an owner-scoped arbiter — every declarer
must agree on the canvas size (a mismatch throws instead of silently
last-writer-wins), and the canvas nulls only once EVERY declarer has
revoked. Both CharacterCreationUiController and CharacterManagementUi-
Controller now declare/revoke instead of writing FixedCanvasSize directly;
grepped for stragglers, none remain in production code (the raw setter
stays public only for UiRootFixedCanvasTests' isolated scale-math
coverage). New test (reviewer-specified):
CharacterScreensFixedCanvasArbiterTests — two controllers sharing one
UiRoot, proving the canvas stays set through chargen's Exit-confirm Close
while char-management is still active, nulling only once char-management
also deactivates, plus the original F1 defect's own covering case (both
revoke together at world entry).

R3 — HeadlessSessionHostTests.ContentLease_InstallsRealChargenOptions_
SelectHeritageIsAccepted proves F6's install actually opens the gate: a
content lease carrying a real hand-built DatCharGen heritage (not
ChargenOptions.Empty) is installed, and TrySelectHeritage for it succeeds.

R2 — filed docs/ISSUES.md #402 for the pre-existing
Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate
full-suite flake (unrelated to Campaign CC).

R4 — fixed "unchached" -> "uncached" typo in
InteractionRetainedUiComposition.cs.

Runtime 1713/0, App 5127/13 skips (+2), Headless 166/0 (+1), full solution
Release build green. Live-DAT probes 7/7 under ACDREAM_PROBE_LIVE_MOUNT=1.
The known #402 flake did not fire across 3 consecutive full-suite runs
this session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 19:04:04 +02:00
parent ec854db045
commit 8add0667a5
9 changed files with 780 additions and 29 deletions

View file

@ -201,16 +201,15 @@ internal sealed class CharacterCreationUiController : IDisposable
Root.ClickThrough = false;
Root.Visible = false;
// AD-98: the same authored 800x600 fixed-canvas treatment as the
// character-management screen. CharacterManagementUiController sets
// UiRoot.FixedCanvasSize ONCE on its own activation edge
// (Tick's `if (!_active)` arm) and NULLS it in both Deactivate AND
// Dispose — it is NOT a per-tick set, and this controller must be
// symmetric with that exact shape (review fix round F1, 2026-08-15
// — the earlier claim here that it was safe to leave the canvas
// pinned forever was FALSE and left an 800x600-scaled canvas
// covering the in-world UI whenever this screen had been opened).
// See Open/Close/Deactivate/Dispose below for the matching set/null
// pair.
// character-management screen. Both controllers DECLARE/REVOKE
// through UiRoot's owner-scoped arbiter (review fix round R1,
// 2026-08-15) rather than writing UiRoot.FixedCanvasSize directly —
// char-management can be simultaneously active underneath this
// screen, and a raw write from either controller is a last-writer-
// wins race with no owner (the F1 fix's own Close() null wiped
// char-management's still-active canvas out from under it). See
// Open/Close/Deactivate/Dispose below for the matching declare/
// revoke pair.
_authoredCanvas = new Vector2(
Root.Width > 0f ? Root.Width : 800f,
Root.Height > 0f ? Root.Height : 600f);
@ -380,17 +379,19 @@ internal sealed class CharacterCreationUiController : IDisposable
/// <summary>Opens the screen at retail's authored default page
/// (<c>gmCharGenMainUI::gmCharGenMainUI</c>'s trailing
/// <c>SetProgressState(this, ECG_HERTAGE)</c>). Sets the fixed canvas
/// on this exact activation edge — matching
/// <see cref="CharacterManagementUiController"/>'s own one-shot set —
/// not per-tick; <see cref="Close"/>/<see cref="Deactivate"/>/
/// <see cref="Dispose"/> null it back out symmetrically.</summary>
/// <c>SetProgressState(this, ECG_HERTAGE)</c>). Declares the fixed
/// canvas on this exact activation edge through <see cref="UiRoot"/>'s
/// arbiter — matching <see cref="CharacterManagementUiController"/>'s
/// own one-shot declare — not per-tick; <see cref="Close"/>/
/// <see cref="Deactivate"/>/<see cref="Dispose"/> revoke it back out
/// symmetrically, and the canvas stays set for as long as ANY other
/// declarer (e.g. character-management underneath) remains active.</summary>
internal void Open()
{
if (_disposed)
return;
_isOpen = true;
_host.FixedCanvasSize = _authoredCanvas;
_host.DeclareFixedCanvas(this, _authoredCanvas);
ApplyProgressState(Page.Heritage);
}
@ -400,7 +401,7 @@ internal sealed class CharacterCreationUiController : IDisposable
return;
_isOpen = false;
Root.Visible = false;
_host.FixedCanvasSize = null;
_host.RevokeFixedCanvas(this);
}
public void Dispose()
@ -415,9 +416,11 @@ internal sealed class CharacterCreationUiController : IDisposable
finally
{
// Matches CharacterManagementUiController.Dispose's own
// unconditional null — defends against disposing while _isOpen
// (Close() is not otherwise called on this path).
_host.FixedCanvasSize = null;
// unconditional revoke — defends against disposing while
// _isOpen (Close() is not otherwise called on this path). Idle
// if Close() already revoked (RevokeFixedCanvas is a no-op for
// an owner that already revoked).
_host.RevokeFixedCanvas(this);
_back.OnClick = null;
_next.OnClick = null;
_finish.OnClick = null;

View file

@ -114,9 +114,13 @@ internal sealed class CharacterManagementUiController : IDisposable
// while this screen is active, the host stretches the ENTIRE canvas —
// widgets, glyphs, and the painted background (which carries the
// "World"/"Characters" captions as art) — as one unit via
// UiRoot.FixedCanvasSize. Resizing the root here instead is exactly the
// half-substitution that misaligned the widgets against the stretched
// art at the 2026-08-15 user gate.
// UiRoot.FixedCanvasSize, declared/revoked through the owner-scoped
// arbiter (review fix round R1, 2026-08-15) rather than written
// directly — character-creation can be simultaneously active on top
// of this screen, and a raw write from either controller is a last-
// writer-wins race with no owner. Resizing the root here instead of
// using the canvas is exactly the half-substitution that misaligned
// the widgets against the stretched art at the 2026-08-15 user gate.
_authoredCanvas = new Vector2(
Root.Width > 0f ? Root.Width : 800f,
Root.Height > 0f ? Root.Height : 600f);
@ -298,7 +302,7 @@ internal sealed class CharacterManagementUiController : IDisposable
{
_active = true;
Root.Visible = true;
_host.FixedCanvasSize = _authoredCanvas;
_host.DeclareFixedCanvas(this, _authoredCanvas);
_host.BringToFront(Root);
}
@ -368,7 +372,7 @@ internal sealed class CharacterManagementUiController : IDisposable
}
finally
{
_host.FixedCanvasSize = null;
_host.RevokeFixedCanvas(this);
_enter.OnClick = null;
_delete.OnClick = null;
_restore.OnClick = null;
@ -752,7 +756,7 @@ internal sealed class CharacterManagementUiController : IDisposable
{
_active = false;
Root.Visible = false;
_host.FixedCanvasSize = null;
_host.RevokeFixedCanvas(this);
}
foreach (UiButton row in _rows)
{