feat(chargen): Campaign CC slice CC6b-MOUNT — Appearance page + preview mount
The page-mount half CC6b-PRE deferred: CharacterCreationAppearancePage (gender buttons, Face/Clothes sub-tabs, nine spin controls with retail's decrement/increment/select-as-current-part OnClickAt zones, nine color swatches, shade scrollbar, zoom/rotate wiring) plus ChargenPreviewController, which bridges the ChargenPreviewRenderer/ChargenPreviewZoomController camera-injection gap CC6a/CC6b-PRE left open and mounts as the third private creature viewport beside paperdoll/creature-appraisal. Color-wheel scouting (campaign risk item 4): live-DAT probe found every color-wheel-family id resolves through existing DatWidgetFactory mappings (Button/Scrollbar/generic fallback) — no new widget type needed. The @140355 gender-flip-on-init oddity (risk item 5): resolved via decomp alone — gmCharGenMainUI's own ctor calls CharGenState::RandomizeCharacter before any page constructs, so retail's chargen screen is never actually blank on open; the Appearance page's gender-flip code always fires against a real, randomly-rolled gender. Filed AP-214 (acdream doesn't port RandomizeCharacter this round, so it opens honestly blank instead) and AP-215 (two narrow visual substitutions: swatch .Selected highlight vs retail's separate overlay, ordinal labels vs retail's icon-only spins). AD-101 retired: the Heritage page's auto-gender-select interim default is deleted now that the Appearance page's real gender buttons exist. TS-82 narrowed to Summary-only. Scope addendum: ChargenPreviewRotationController's parameterless-constructor default changes from 0f to a new RetailDefaultHeadingDegrees=180f constant (retail's InitializePage override, not the ctor's raw 0) — every real gmCG3DView owner converges on 180 before its first frame, so a controller defaulting to 0 was a trap for future consumers. Runtime 1713/0, Core 4786/1 skip, Content 147/0, App 5220/3 skips (Release, ACDREAM_PROBE_LIVE_MOUNT=1) — zero failures across two clean full-solution runs; the one Core.Net.Tests NakEmissionTests flake observed on a third run is the same pre-existing, previously-documented timing flake (zero files under src/AcDream.Core.Net/ touched, passes 100% in isolation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
11374484dc
commit
34c6fceab0
20 changed files with 2109 additions and 57 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Content;
|
||||
|
|
@ -314,6 +315,131 @@ public sealed class CharacterCreationLiveDatTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC slice CC6b-MOUNT — the Appearance page's full authored
|
||||
/// widget catalog. Pins the campaign plan's risk item 4 finding (the
|
||||
/// color-wheel/gradient family resolves through EXISTING
|
||||
/// <c>DatWidgetFactory</c> mappings; no new widget type was needed —
|
||||
/// see <see cref="CharacterCreationAppearancePage"/>'s own class doc)
|
||||
/// against the real installed DAT: gender/Face/Clothes buttons, all
|
||||
/// nine spins, all nine color swatches, the shade scrollbar, and the
|
||||
/// viewport.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_HasGenderChoiceSpinsSwatchesShadeAndViewport()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
|
||||
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FemaleButtonId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.MaleButtonId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.FaceButtonId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ClothesButtonId);
|
||||
Assert.IsAssignableFrom<UiElement>(
|
||||
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.FaceChoicesId));
|
||||
Assert.IsAssignableFrom<UiElement>(
|
||||
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ClothesChoicesId));
|
||||
|
||||
foreach (uint spinId in new[]
|
||||
{
|
||||
CharacterCreationAppearancePage.HairSpinId,
|
||||
CharacterCreationAppearancePage.EyesSpinId,
|
||||
CharacterCreationAppearancePage.NoseSpinId,
|
||||
CharacterCreationAppearancePage.MouthSpinId,
|
||||
CharacterCreationAppearancePage.SkinSpinId,
|
||||
CharacterCreationAppearancePage.HeadgearSpinId,
|
||||
CharacterCreationAppearancePage.ShirtSpinId,
|
||||
CharacterCreationAppearancePage.TrousersSpinId,
|
||||
CharacterCreationAppearancePage.FootwearSpinId,
|
||||
})
|
||||
{
|
||||
AssertButton(appearanceRoot, spinId);
|
||||
}
|
||||
|
||||
// Every color-wheel-family id resolves through EXISTING
|
||||
// DatWidgetFactory mappings (Button=1, Scrollbar=0xB, the generic
|
||||
// Type-3 fallback) — the risk-item-4 scouting result, pinned.
|
||||
foreach (uint swatchId in CharacterCreationAppearancePage.SwatchIds)
|
||||
AssertButton(appearanceRoot, swatchId);
|
||||
Assert.IsType<UiScrollbar>(
|
||||
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ShadeScrollId));
|
||||
Assert.IsType<UiDatElement>(
|
||||
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.GradCircleId));
|
||||
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateClockwiseId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.RotateCounterClockwiseId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomInId);
|
||||
AssertButton(appearanceRoot, CharacterCreationAppearancePage.ZoomOutId);
|
||||
|
||||
Assert.IsType<UiViewport>(
|
||||
UiElement.FindDescendant(appearanceRoot, CharacterCreationAppearancePage.ViewportId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live-DAT-measured arrow geometry the page's spin OnClickAt zones are
|
||||
/// built from — every one of the nine spins is uniformly 200px wide
|
||||
/// with the two locally-reused arrow child ids
|
||||
/// (<c>0x1000030a</c> decrement / <c>0x1000030b</c> increment) at the
|
||||
/// SAME local positions. If a future DAT revision changes this shared
|
||||
/// template's geometry, this test (not a silent behavior change) is
|
||||
/// where it shows up.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_SpinArrowGeometryIsUniformAcrossAllNineSpins()
|
||||
{
|
||||
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));
|
||||
ElementInfo? appearanceInfo = FindInfo(
|
||||
rootInfo, CharacterCreationUiController.AppearancePageElementId);
|
||||
Assert.NotNull(appearanceInfo);
|
||||
|
||||
foreach (uint spinId in new[]
|
||||
{
|
||||
CharacterCreationAppearancePage.HairSpinId,
|
||||
CharacterCreationAppearancePage.EyesSpinId,
|
||||
CharacterCreationAppearancePage.NoseSpinId,
|
||||
CharacterCreationAppearancePage.MouthSpinId,
|
||||
CharacterCreationAppearancePage.SkinSpinId,
|
||||
CharacterCreationAppearancePage.HeadgearSpinId,
|
||||
CharacterCreationAppearancePage.ShirtSpinId,
|
||||
CharacterCreationAppearancePage.TrousersSpinId,
|
||||
CharacterCreationAppearancePage.FootwearSpinId,
|
||||
})
|
||||
{
|
||||
ElementInfo? spin = FindInfo(appearanceInfo!, spinId);
|
||||
Assert.NotNull(spin);
|
||||
Assert.Equal(200f, spin!.Width);
|
||||
|
||||
ElementInfo? decrement = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Au);
|
||||
ElementInfo? increment = spin.Children.FirstOrDefault(c => c.Id == 0x1000030Bu);
|
||||
Assert.NotNull(decrement);
|
||||
Assert.NotNull(increment);
|
||||
Assert.Equal(80f, decrement!.X);
|
||||
Assert.Equal(127f, increment!.X);
|
||||
}
|
||||
}
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo node, uint id)
|
||||
{
|
||||
if (node.Id == id) return node;
|
||||
foreach (ElementInfo child in node.Children)
|
||||
{
|
||||
ElementInfo? found = FindInfo(child, id);
|
||||
if (found is not null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RetailDialogFactory MakeDialogFactory(IDatReaderWriter dats, UiRoot host)
|
||||
{
|
||||
uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u);
|
||||
|
|
@ -331,6 +457,9 @@ public sealed class CharacterCreationLiveDatTests
|
|||
private static void AssertButton(ImportedLayout layout, uint elementId) =>
|
||||
Assert.IsType<UiButton>(layout.FindElement(elementId));
|
||||
|
||||
private static void AssertButton(UiElement root, uint elementId) =>
|
||||
Assert.IsType<UiButton>(UiElement.FindDescendant(root, elementId));
|
||||
|
||||
private static ImportedLayout BuildSelected(
|
||||
IDatReaderWriter dats,
|
||||
uint layoutDid,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue