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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.UI;
@ -41,9 +42,94 @@ public sealed class UiRoot : UiElement
/// renderer's quad chokepoint; the mouse entry points apply the inverse,
/// so <see cref="MouseX"/>/<see cref="MouseY"/> and every hit test live
/// in canvas space. Null (the in-world default) is native 1:1.
///
/// <para>
/// Campaign CC slice CC4 review-fix round R1 (2026-08-15): this raw
/// setter remains public for tests that exercise the scale/mouse-
/// mapping math in isolation (<c>UiRootFixedCanvasTests</c>), but
/// PRODUCTION code must go through <see cref="DeclareFixedCanvas"/>/
/// <see cref="RevokeFixedCanvas"/> instead of writing this property
/// directly. Two fixed-canvas screens can be active at once
/// (character-management underneath character-creation) and a raw
/// write from either one is a last-writer-wins race with no owner —
/// the F1 fix's own <c>Close()</c> null wiped the OTHER screen's still-
/// active canvas out from under it (see AD-98).
/// </para>
/// </summary>
public Vector2? FixedCanvasSize { get; set; }
/// <summary>Screens currently declaring a fixed canvas, keyed by owner
/// (see <see cref="DeclareFixedCanvas"/>).</summary>
private readonly Dictionary<object, Vector2> _fixedCanvasDeclarations = new();
/// <summary>
/// Declares that <paramref name="owner"/> wants the retained tree laid
/// out in <paramref name="size"/> while it is active. This is the single
/// arbiter for <see cref="FixedCanvasSize"/>: multiple owners may declare
/// concurrently (character-management stays declared while character-
/// creation is also open on top of it), and the effective
/// <see cref="FixedCanvasSize"/> is the shared declaration set's value.
/// Every current declarer must agree on the size — a mismatched second
/// declaration throws rather than silently overwriting the first
/// (Campaign CC CC4 review-fix round R1, 2026-08-15; see
/// <c>docs/architecture/retail-divergence-register.md</c> AD-98). Pair
/// every call with <see cref="RevokeFixedCanvas"/> on the SAME owner at
/// deactivate/close/dispose.
/// </summary>
public void DeclareFixedCanvas(object owner, Vector2 size)
{
ArgumentNullException.ThrowIfNull(owner);
if (_fixedCanvasDeclarations.TryGetValue(owner, out Vector2 existing))
{
if (existing == size)
return; // idempotent re-declare (e.g. a re-ticked activation edge)
throw new InvalidOperationException(
$"UiRoot.DeclareFixedCanvas: owner {owner} re-declared a different " +
$"canvas ({existing} -> {size}) without revoking first.");
}
foreach (Vector2 declared in _fixedCanvasDeclarations.Values)
{
if (declared != size)
{
throw new InvalidOperationException(
$"UiRoot.DeclareFixedCanvas: owner {owner} declared {size} but " +
$"another active owner already declared {declared} — every " +
"concurrently-active fixed-canvas screen must author the SAME " +
"canvas size (see AD-98).");
}
}
_fixedCanvasDeclarations[owner] = size;
FixedCanvasSize = size;
}
/// <summary>Revokes <paramref name="owner"/>'s declaration from
/// <see cref="DeclareFixedCanvas"/>. <see cref="FixedCanvasSize"/>
/// becomes null only once EVERY declarer has revoked; while another
/// owner is still declared, it stays set to that shared value. A
/// revoke from an owner that never declared (or already revoked) is a
/// no-op, matching the idempotent shutdown paths (<c>Deactivate</c>
/// AND <c>Dispose</c> can both revoke the same owner).</summary>
public void RevokeFixedCanvas(object owner)
{
ArgumentNullException.ThrowIfNull(owner);
if (!_fixedCanvasDeclarations.Remove(owner))
return;
if (_fixedCanvasDeclarations.Count == 0)
{
FixedCanvasSize = null;
return;
}
foreach (Vector2 declared in _fixedCanvasDeclarations.Values)
{
FixedCanvasSize = declared;
break;
}
}
/// <summary>
/// The coordinate space the retained tree currently lays out in: the fixed
/// authored canvas while one is active, else the window itself. Anything