fix(chargen): Campaign CC gate round 1 Batch A — GF-15 input, GF-5 skills rows, GF-13 GM toggles
GF-15 (the gate blocker): the Summary name field and Finish button were
NOT structurally broken — live repro over the project's own local ACE
test server showed clicks correctly focus the field and land characters.
The real bug only surfaces after the first dialog opens: pressing Finish
empty successfully creates the NoName RetailMessageDialogView (visible,
correct 400x95 geometry) but it renders nothing and silently absorbs
every click across the whole canvas. Root cause: CharacterCreationUiController.Tick
and CharacterManagementUiController.Tick both call UiRoot.BringToFront(Root)
unconditionally every frame (needed so chargen stays above the occluded
management screen, AP-229); a dialog root is a direct sibling under the
same UiRoot, and RetailWindowManager.BringToFront is "highest ZOrder among
siblings + 1" — whichever BringToFront runs last in a frame wins.
RetailDialogFactory.Tick never re-asserted its own dialogs' z-order, so
the next frame's screen Tick buried the dialog behind the screen's opaque
backdrop while it stayed the registered Modal with exclusive input
priority. Fixed by having RetailDialogFactory.Tick re-raise every open
dialog (in open-order) each tick, matching retail's always-on-top dialog
behavior. Live-verified the complete user sequence end to end: click
field, type, press Finish empty, dialog now visibly renders, OK dismisses
cleanly, field still typable afterward. The "[ Name" prefill question is
closed as a non-bug: neither CharGenState::RandomizeCharacter nor
gmCGSummaryPage::InitializePage write text into the field in the decomp;
retail's field is genuinely empty on open, matching acdream already.
GF-5: CharacterCreationSkillsPage.RebuildRows resolved the wrong listbox
template (Templates[0], retail's own 3-child bucket-header row) and
required the root to be a UiButton (it's a plain container). Byte-traced
gmCGSkillsPage::DoSkillRecords + tagSkillRecord's copy-ctor field order
to map every child id in the real row (Templates[1]): name, level/cost
text, and the two real per-row up/down arrow buttons. Wired the arrows to
retail's own plain-click dispatch, retiring (narrowing) AP-213's
click-to-advance/double-click-retreat single-button substitution.
GF-13: dat property 0x3B (Invisible) was never read by the importer.
Elements 0x10000403/0x10000494 ("Non-Admin"/"Non-Envoy") author it true.
A blast-radius sweep found 1,083 elements client-wide author the same
flag, so this fix stays chargen-scoped only (ElementInfo.Invisible /
UiElement.AuthoredInvisible are pure data additions; only
CharacterCreationUiController acts on them, by the authored flag, not a
hardcoded id list). General importer-wide honor filed as ISSUES.md #408;
register row AP-230 records the split.
Gates: solution build green; App 5266/3 skips/0 failed; Runtime 1735/0;
full-solution run 0 failures anywhere. Register: AP-230 filed, AP-213
narrowed. ISSUES: #408 filed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6699e0f88c
commit
1d9de5e095
11 changed files with 823 additions and 78 deletions
|
|
@ -617,6 +617,189 @@ public sealed class CharacterCreationLiveDatTests
|
|||
+ "gate argument needs re-verification for this skill.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-13 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed:
|
||||
/// the GM-only labels <c>0x10000403</c> ("Non-Admin") and
|
||||
/// <c>0x10000494</c> ("Non-Envoy") both live under the Summary page
|
||||
/// (<c>0x100003D6</c>, path <c>0x100003CC > 0x100003D0 >
|
||||
/// 0x100003D6 > {0x10000403,0x10000494}</c>) and both author dat
|
||||
/// property <c>0x3B</c> (Invisible) = <see langword="true"/> — the exact
|
||||
/// mechanism retail's <c>UIElement::OnSetAttribute @0x00462d80</c> case 8
|
||||
/// hides them by. Pins the DATA half (<see cref="ElementInfo.Invisible"/>)
|
||||
/// against the installed EoR dat.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SummaryPage_NonAdminNonEnvoyLabels_AuthorInvisibleTrue()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId));
|
||||
|
||||
foreach (uint targetId in new[] { 0x10000403u, 0x10000494u })
|
||||
{
|
||||
ElementInfo? found = FindInfo(rootInfo, targetId);
|
||||
Assert.NotNull(found);
|
||||
Assert.True(
|
||||
found!.Invisible,
|
||||
$"element 0x{targetId:X8} must author dat property 0x3B (Invisible) = true.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-13: the BEHAVIOR half — after the real controller mounts through
|
||||
/// <see cref="CharacterCreationUiController.CreateDetached"/> (not a raw
|
||||
/// <see cref="LayoutImporter.Build"/> call), the two authored-invisible
|
||||
/// elements are not <see cref="UiElement.Visible"/>. Exercises
|
||||
/// <c>HideAuthoredInvisibleElements</c>'s real chargen-scoped honor path,
|
||||
/// not just the data plumbing the sibling test above pins.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SummaryPage_NonAdminNonEnvoyLabels_HiddenAfterControllerMount()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
var host = new UiRoot();
|
||||
var dialogs = MakeDialogFactory(dats, host);
|
||||
var bindings = new CharacterCreationRuntimeBindings(
|
||||
() => null,
|
||||
_ => default,
|
||||
_ => default,
|
||||
_ => default,
|
||||
(_, _) => default,
|
||||
(_, _) => default,
|
||||
_ => default,
|
||||
_ => default,
|
||||
_ => default,
|
||||
_ => default,
|
||||
_ => default,
|
||||
() => { });
|
||||
|
||||
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId) =>
|
||||
LayoutImporter.Import(
|
||||
dats, templateLayoutId, templateElementId, _ => (0u, 0, 0), null)?.Root;
|
||||
|
||||
CharacterCreationUiController? controller =
|
||||
CharacterCreationUiController.CreateDetached(
|
||||
host, screen, ResolveTemplate, dialogs, bindings,
|
||||
new CharacterCreationUiController.DialogStrings(
|
||||
"Are you sure?", "No name", "Unspent credits", "Randomize?", "Name too long"));
|
||||
Assert.NotNull(controller);
|
||||
|
||||
foreach (uint targetId in new[] { 0x10000403u, 0x10000494u })
|
||||
{
|
||||
UiElement found = Assert.IsAssignableFrom<UiElement>(
|
||||
UiElement.FindDescendant(controller!.Root, targetId));
|
||||
Assert.True(found.AuthoredInvisible, $"0x{targetId:X8} must carry AuthoredInvisible.");
|
||||
Assert.False(found.Visible, $"0x{targetId:X8} must be hidden after mount.");
|
||||
}
|
||||
|
||||
controller!.Dispose();
|
||||
dialogs.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-5 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
|
||||
/// (raw <c>ElementDesc.Type</c> — the same id space as retail's
|
||||
/// <c>DynamicCast</c> tags, 1=Button, 12=Text): the Skills listbox
|
||||
/// authors exactly two templates. <c>Templates[0]</c>
|
||||
/// (<c>0x100002F4</c>) is retail's own 3-child bucket-HEADER row
|
||||
/// (unused by this port's flat-list simplification, AP-213).
|
||||
/// <c>Templates[1]</c> (<c>0x100002FF</c>) is the REAL skill row — a
|
||||
/// plain container root (rawType 3, NOT a Button), 7 children, byte-
|
||||
/// traced against <c>gmCGSkillsPage::DoSkillRecords @ 0x004817e0</c> +
|
||||
/// <c>tagSkillRecord</c>'s copy-constructor field order
|
||||
/// (<c>acclient.h</c>): name (<c>0x10000301</c>, Text), pSkillLevelText
|
||||
/// (<c>0x10000302</c>, Text), pUpCostText (<c>0x10000303</c>, Text),
|
||||
/// pSkillUpButton (<c>0x10000304</c>, Button), pSkillDownButton
|
||||
/// (<c>0x10000305</c>, Button), pDownCostText (<c>0x10000306</c>, Text).
|
||||
/// See <see cref="CharacterCreationSkillsPage"/>'s own class doc for the
|
||||
/// full trace.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsPage_RealRowTemplate_HasNameLevelCostTextAndArrowButtons()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
|
||||
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
|
||||
|
||||
Assert.Equal(2, list.Templates.Count);
|
||||
|
||||
UiTemplateListEntry realRowTemplate = list.Templates[1];
|
||||
Assert.Equal(0x100002FFu, realRowTemplate.TemplateElementId);
|
||||
|
||||
UiElement? row = LayoutImporter.Import(
|
||||
dats,
|
||||
realRowTemplate.TemplateLayoutId,
|
||||
realRowTemplate.TemplateElementId,
|
||||
_ => (0u, 0, 0),
|
||||
null)?.Root;
|
||||
UiElement realRow = Assert.IsAssignableFrom<UiElement>(row);
|
||||
Assert.IsNotType<UiButton>(realRow);
|
||||
|
||||
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000301u));
|
||||
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000302u));
|
||||
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000303u));
|
||||
Assert.IsType<UiText>(UiElement.FindDescendant(realRow, 0x10000306u));
|
||||
Assert.IsType<UiButton>(UiElement.FindDescendant(realRow, 0x10000304u));
|
||||
Assert.IsType<UiButton>(UiElement.FindDescendant(realRow, 0x10000305u));
|
||||
|
||||
// Templates[0] (0x100002F4) is retail's bucket-header row — unused
|
||||
// by RebuildRows, still confirmed present so a future revision that
|
||||
// drops it or changes its shape shows up here.
|
||||
Assert.Equal(0x100002F4u, list.Templates[0].TemplateElementId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-15 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
|
||||
/// during the investigation: the Message dialog catalog's popup
|
||||
/// (<c>0x3D</c>), message text (<c>0x3E</c>), and OK button
|
||||
/// (<c>0x26</c>) all author REAL, nonzero geometry (popup 400x95,
|
||||
/// centered by <see cref="RetailMessageDialogView.SizeAndCenter"/>) —
|
||||
/// ruling out a zero-size/collapsed-layout explanation for the dialog
|
||||
/// rendering nothing. The actual root cause was a Z-ORDER bug (the
|
||||
/// chargen screen's own per-tick <c>BringToFront</c> burying the dialog
|
||||
/// behind its opaque backdrop while the dialog kept exclusive
|
||||
/// <see cref="UiRoot.Modal"/> input priority — fixed in
|
||||
/// <see cref="RetailDialogFactory.Tick"/>). This test pins the geometry
|
||||
/// half so a future DAT revision that collapses the popup/message/button
|
||||
/// to zero size is caught here instead of silently reintroducing an
|
||||
/// invisible dialog.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void MessageDialogCatalog_PopupMessageAndOkButton_AuthorNonzeroGeometry()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u);
|
||||
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(dats, dialogDid, 0x24u));
|
||||
Assert.Equal(800f, rootInfo.Width);
|
||||
Assert.Equal(600f, rootInfo.Height);
|
||||
|
||||
ElementInfo popup = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x3Du));
|
||||
Assert.True(popup.Width > 0f && popup.Height > 0f);
|
||||
|
||||
ElementInfo message = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x3Eu));
|
||||
Assert.True(message.Width > 0f && message.Height > 0f);
|
||||
|
||||
ElementInfo okButton = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x26u));
|
||||
Assert.True(okButton.Width > 0f && okButton.Height > 0f);
|
||||
}
|
||||
|
||||
private static void AssertButton(ImportedLayout layout, uint elementId) =>
|
||||
Assert.IsType<UiButton>(layout.FindElement(elementId));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue