fix(app,runtime,headless): Campaign CC slice CC4 review-fix round — F1-F12
Dual-lens review of CC4 (0e71d3b8) returned architectural FAIL (F1, F6)
and retail-fidelity PASS-with-reservations (F2, F3, F4), plus LOW
findings F5, F7-F12. F13 (TS-82's merge collision with campaign-cc6a) is
merge mechanics for the orchestrator, not addressed here.
F1 (HIGH, blocking): CharacterCreationUiController never released
UiRoot.FixedCanvasSize, on a FALSE premise that CharacterManagementUi-
Controller does a per-tick set (it does not — it sets once on activation
and nulls on Deactivate/Dispose). Root cause: RuntimeCharacterCreation-
State had no CompleteEnter() analogue to RuntimeCharacterSelectionState's,
so the creation view reported IsActive=true for an entire in-world
session. Added CompleteEnter(), wired at both LiveSessionController
in-world edges (StartCore, EnterHighlightedCore); made Open/Close/
Deactivate/Dispose set/null the canvas symmetrically; corrected the false
comment and ledger claim; added FixedCanvasSize test coverage.
F2 (MEDIUM-HIGH, blocking): the attribute-slider scalar mapping was not
retail's. Fixed display to value/100f (UpdateAttributeValues @
0x0048251d) and the drag inverse to truncate+clamp-low-only, no rescale
(ListenToElementMessage @ 0x004829c0, independently re-verified against
the decomp). Added tests at scalar 0.5/0.0 plus a display-direction test.
F3 (MEDIUM, blocking): ported the unported heritage-button tab-restore
arm (ListenToElementMessage @ 0x004e9450) — SHOW/HIDE id sets independently
re-derived from the decomp, including the genuine Lugian (0x100005f1)
no-restore quirk, reproduced faithfully. Wired via a new HeritagePage
click callback; added restore + quirk tests.
F4 (MEDIUM): ported SetTown's (@ 0x0047c360) separate per-town page-root
state literal (Holtburg->0x10000034 etc.), independently re-derived from
the decomp's tail-merged branches; wired via the existing
IUiDatStateful.TrySetRetailState seam; added a test.
F5 (MEDIUM): softened AD-103's unmeasured pixel-equivalence claim.
F6 (MEDIUM, blocking): DECISION — install ChargenOptions in the headless
content path (chosen over marking headless creation out-of-scope).
HeadlessSessionHost now calls InstallOptions off the shared content
lease's Dats, beside the existing InstallSpellMetadata call.
F7: AP-213 already named the label format and click/double-click
substitution explicitly on inspection — no edit needed.
F8: AP-212 now names all six DoRandom primitives with a known landing site.
F9: AD-101 retirement corrected to precede CC5's Finish un-ghosting.
F10: merged ItemAppraisalTextFormatter's duplicate <summary> block.
F11: fixed TS-82's wrong AP-211 cross-reference.
F12: cached the chargen DatStringResolver once per composition instead of
per ResolveText call.
Runtime 1713/0, App 5125/13 skips (+8 new tests), Headless 165/0, full
solution Release build green. Live-DAT probes 7/7 under
ACDREAM_PROBE_LIVE_MOUNT=1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0e71d3b829
commit
ec854db045
12 changed files with 419 additions and 30 deletions
|
|
@ -312,6 +312,192 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Assert.Equal(2, environment.Runtime.LastSelectedStartArea);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F4 (2026-08-15): <c>gmCGTownPage::SetTown
|
||||
/// @ 0x0047c360</c> also sets the TOWN PAGE'S OWN retail state via a
|
||||
/// literal per-town map — Holtburg->0x10000034, Yaraq->0x10000036 —
|
||||
/// SEPARATE from the master page's per-page-index cycling
|
||||
/// (<c>0x10000025+page</c>, already covered by the page-switch tests
|
||||
/// above).</summary>
|
||||
[Fact]
|
||||
public void TownButton_Refresh_SetsThePagesOwnRetailStateLiteral()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.TabButton(CharacterCreationUiController.TownTabElementId)
|
||||
.OnClick!();
|
||||
|
||||
var pageRoot = Assert.IsType<UiDatElement>(
|
||||
environment.Page(CharacterCreationUiController.TownPageElementId));
|
||||
|
||||
environment.Button(0x1000040Du).OnClick!(); // Holtburg -> startArea 0
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Holtburg", pageRoot.ActiveState);
|
||||
|
||||
environment.Button(0x1000040Eu).OnClick!(); // Yaraq -> startArea 2
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Yaraq", pageRoot.ActiveState);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F2 (2026-08-15), the display direction:
|
||||
/// <c>gmCGProfessionPage::UpdateAttributeValues @ 0x0048251d</c> sets
|
||||
/// the slider's scalar position to <c>value * 0.00999999978f</c>
|
||||
/// (value/100), not a [10,100]-to-[0,1] rescale.</summary>
|
||||
[Fact]
|
||||
public void ProfessionSlider_Refresh_DisplaysScalarAsValueOverOneHundred()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId)
|
||||
.OnClick!();
|
||||
|
||||
UiElement strengthContainer = Assert.IsAssignableFrom<UiElement>(
|
||||
environment.Screen.FindElement(0x100003E6u));
|
||||
var slider = Assert.IsType<UiScrollbar>(
|
||||
UiElement.FindDescendant(strengthContainer, 0x100002EEu));
|
||||
|
||||
RuntimeCharacterCreationSnapshot snapshot = environment.Runtime.View.Snapshot;
|
||||
environment.Runtime.View.Snapshot = snapshot with
|
||||
{
|
||||
Revision = snapshot.Revision + 1,
|
||||
Attributes = snapshot.Attributes with { Strength = 55 },
|
||||
};
|
||||
environment.Controller.Tick();
|
||||
|
||||
Assert.Equal(0.55f, slider.ScalarPosition);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F2 (2026-08-15), the drag-inverse
|
||||
/// direction: <c>ListenToElementMessage @ 0x004829c0</c>'s scrollbar-
|
||||
/// drag case truncates <c>scalar*100</c> and clamps LOW only to 10 —
|
||||
/// NOT the [10,100]<->[0,1] rescale the previous (wrong) formula
|
||||
/// used, which only coincidentally agreed with the correct one at
|
||||
/// scalar=1 (the pre-existing
|
||||
/// <see cref="ProfessionSlider_ScalarChange_SetsTheAttribute"/> case).</summary>
|
||||
[Theory]
|
||||
[InlineData(0.5f, 50)]
|
||||
[InlineData(0f, 10)]
|
||||
public void ProfessionSlider_ScalarChange_TruncatesAndClampsLowOnly(
|
||||
float scalar,
|
||||
int expectedValue)
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId)
|
||||
.OnClick!();
|
||||
|
||||
UiElement strengthContainer = Assert.IsAssignableFrom<UiElement>(
|
||||
environment.Screen.FindElement(0x100003E6u));
|
||||
var slider = Assert.IsType<UiScrollbar>(
|
||||
UiElement.FindDescendant(strengthContainer, 0x100002EEu));
|
||||
|
||||
slider.ScalarChanged!(scalar);
|
||||
|
||||
Assert.Equal(ChargenAttributeId.Strength, environment.Runtime.LastAttributeSet);
|
||||
Assert.Equal(expectedValue, environment.Runtime.LastAttributeValue);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F3 (2026-08-15):
|
||||
/// <c>gmCharGenMainUI::ListenToElementMessage @ 0x004e9450</c>'s
|
||||
/// heritage-button bubble arm shows/hides the Profession/Skills/Town
|
||||
/// tabs SYNCHRONOUSLY at click time — independent of
|
||||
/// <see cref="OlthoiHeritage_HidesProfessionSkillsAndTownTabs"/>'s
|
||||
/// page-switch-time recompute (no tab/Back/Next click happens in this
|
||||
/// test at all). Lugian (<c>0x100005f1</c>) sits outside BOTH the SHOW
|
||||
/// and HIDE case lists in the decompiled switch — a genuine retail
|
||||
/// quirk, reproduced faithfully.</summary>
|
||||
[Fact]
|
||||
public void HeritageButtonClick_RestoresHiddenTabsAtClickTime_ExceptLugian()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
|
||||
environment.Button(0x100005C7u).OnClick!(); // Olthoi -> HIDE
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.ProfessionTabElementId).Visible);
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.SkillsTabElementId).Visible);
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.TownTabElementId).Visible);
|
||||
|
||||
environment.Button(0x100005F1u).OnClick!(); // Lugian -> no-op quirk
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.ProfessionTabElementId).Visible);
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.SkillsTabElementId).Visible);
|
||||
Assert.False(environment.TabButton(
|
||||
CharacterCreationUiController.TownTabElementId).Visible);
|
||||
|
||||
environment.Button(0x100003BFu).OnClick!(); // Aluvian -> SHOW
|
||||
Assert.True(environment.TabButton(
|
||||
CharacterCreationUiController.ProfessionTabElementId).Visible);
|
||||
Assert.True(environment.TabButton(
|
||||
CharacterCreationUiController.SkillsTabElementId).Visible);
|
||||
Assert.True(environment.TabButton(
|
||||
CharacterCreationUiController.TownTabElementId).Visible);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F1 (2026-08-15): <c>Open()</c> sets
|
||||
/// <c>UiRoot.FixedCanvasSize</c> once on the activation edge (matching
|
||||
/// <c>CharacterManagementUiController</c>'s real, non-per-tick shape);
|
||||
/// <c>Close()</c>/<c>Deactivate()</c>/<c>Dispose()</c> null it back out
|
||||
/// symmetrically. Before this fix nothing ever nulled it, so an
|
||||
/// 800x600-scaled canvas silently covered the in-world UI for the rest
|
||||
/// of the session once this screen had ever been opened.</summary>
|
||||
[Fact]
|
||||
public void Open_SetsFixedCanvas_ExitConfirmClosesAndNullsIt()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
Assert.Null(environment.Host.FixedCanvasSize);
|
||||
|
||||
environment.Controller.Open();
|
||||
Assert.Equal(new Vector2(800f, 600f), environment.Host.FixedCanvasSize);
|
||||
|
||||
environment.Button(CharacterCreationUiController.ExitElementId).OnClick!();
|
||||
environment.ConfirmActiveDialog(confirmed: true);
|
||||
|
||||
Assert.Null(environment.Host.FixedCanvasSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deactivate_NullsFixedCanvas_AndClosesTheScreen()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
Assert.NotNull(environment.Host.FixedCanvasSize);
|
||||
|
||||
// Runtime reporting the view inactive/gone (e.g. entering the
|
||||
// world) must Deactivate -- previously nothing drove this because
|
||||
// RuntimeCharacterCreationState had no CompleteEnter() analogue;
|
||||
// this test exercises the CONTROLLER side of that fix directly by
|
||||
// simulating the view disappearing.
|
||||
environment.Runtime.ProvideView = false;
|
||||
environment.Controller.Tick();
|
||||
|
||||
Assert.False(environment.Controller.Root.Visible);
|
||||
Assert.Null(environment.Host.FixedCanvasSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_NullsFixedCanvas()
|
||||
{
|
||||
var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
Assert.NotNull(environment.Host.FixedCanvasSize);
|
||||
|
||||
environment.Dispose();
|
||||
|
||||
Assert.Null(environment.Host.FixedCanvasSize);
|
||||
}
|
||||
|
||||
private static void BumpRevisionAndTick(EnvironmentHarness environment)
|
||||
{
|
||||
RuntimeCharacterCreationSnapshot snapshot = environment.Runtime.View.Snapshot;
|
||||
environment.Runtime.View.Snapshot = snapshot with { Revision = snapshot.Revision + 1 };
|
||||
environment.Controller.Tick();
|
||||
}
|
||||
|
||||
private static IEnumerable<UiElement> Descendants(UiElement root)
|
||||
{
|
||||
yield return root;
|
||||
|
|
@ -680,6 +866,7 @@ public sealed class CharacterCreationUiControllerTests
|
|||
};
|
||||
page.Children.Add(ButtonInfo(0x100003BFu)); // Aluvian
|
||||
page.Children.Add(ButtonInfo(0x100005C7u)); // Olthoi
|
||||
page.Children.Add(ButtonInfo(0x100005F1u)); // Lugian (F3 quirk: no tab-restore/hide)
|
||||
page.Children.Add(TextInfo(0x100003C4u));
|
||||
return page;
|
||||
}
|
||||
|
|
@ -749,6 +936,14 @@ public sealed class CharacterCreationUiControllerTests
|
|||
page.Children.Add(ButtonInfo(0x1000040Eu)); // Yaraq
|
||||
page.Children.Add(ButtonInfo(0x1000040Fu)); // Shoushi
|
||||
page.Children.Add(TextInfo(0x10000409u));
|
||||
|
||||
// F4: the page ROOT's own retail state literal map
|
||||
// (gmCGTownPage::SetTown @ 0x0047c360), a separate state machine
|
||||
// from the master page's per-page-index cycling.
|
||||
page.States[0x10000034u] = new UiStateInfo { Id = 0x10000034u, Name = "Holtburg" };
|
||||
page.States[0x10000035u] = new UiStateInfo { Id = 0x10000035u, Name = "Sanamar" };
|
||||
page.States[0x10000036u] = new UiStateInfo { Id = 0x10000036u, Name = "Yaraq" };
|
||||
page.States[0x10000037u] = new UiStateInfo { Id = 0x10000037u, Name = "Shoushi" };
|
||||
return page;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue