Revert "Campaign V slice V4a" - it lost world multisampling
This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ceec3bc440
commit
9aaf97e785
334 changed files with 3841 additions and 3661 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
|
@ -6,14 +6,14 @@ using AcDream.App.UI;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the Character window's <b>Attributes tab</b> — LayoutDesc 0x2100002E,
|
||||
/// Controller for the Character window's <b>Attributes tab</b> — LayoutDesc 0x2100002E,
|
||||
/// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root
|
||||
/// type 0x1000002A) which in turn chains into the gmStatManagementUI header content.
|
||||
///
|
||||
/// <para>Unlike <see cref="CharacterController"/> (which targets the SEPARATE text-report
|
||||
/// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element),
|
||||
/// this controller binds the <b>real, statically-mounted</b> header + list elements that the
|
||||
/// importer already produces — every id below is confirmed present via
|
||||
/// importer already produces — every id below is confirmed present via
|
||||
/// <see cref="ImportedLayout.FindElement"/>.</para>
|
||||
///
|
||||
/// <para>Ported from <c>gmStatManagementUI::UpdateCharacterInfo</c> (0x004f0770) +
|
||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>gmAttributeUI::PostInit</c> (0x0049db70) + <c>AttributeInfoRegion</c> / <c>Attribute2ndInfoRegion</c>
|
||||
/// (0x004f1910 / 0x004f19e0). Row icons loaded via sub-element 0x10000129 in the retail
|
||||
/// dat template (each icon is a <c>0x06xxxxxx</c> RenderSurface DataID from SubMap
|
||||
/// 0x25000006 / 0x25000007, spec §2).</para>
|
||||
/// 0x25000006 / 0x25000007, spec §2).</para>
|
||||
///
|
||||
/// <para>Footer State A (nothing selected) bound from
|
||||
/// <c>DisplayDefaultFooter</c> (0x0049cde0): title empty, line-1 value =
|
||||
|
|
@ -39,13 +39,13 @@ namespace AcDream.App.UI.Layout;
|
|||
/// UIStateId.Closed (0x0B). The imported Type-12 tab owns its authored font color and
|
||||
/// propagates the state to its three chrome children through PassToChildren.</para>
|
||||
///
|
||||
/// <para>Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
|
||||
/// <para>Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
|
||||
/// (UIStateId.Normal, 0x01), state "Ghosted" = unaffordable or no selection
|
||||
/// (UIStateId.Ghosted, 0x0D). Source: gmAttributeUI::AttributeInfoRegion::Update (0x004f1910).</para>
|
||||
/// </summary>
|
||||
internal static class CharacterStatController
|
||||
public static class CharacterStatController
|
||||
{
|
||||
// ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
|
||||
// ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
|
||||
public const uint NameId = 0x10000231u; // m_pNameText
|
||||
public const uint HeritageId = 0x10000232u; // m_pHeritageText
|
||||
public const uint PkStatusId = 0x10000233u; // m_pPKStatusText
|
||||
|
|
@ -64,16 +64,16 @@ internal static class CharacterStatController
|
|||
public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter
|
||||
public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer
|
||||
|
||||
// ── Footer STATE-A container id ──────────────────────────────────────────
|
||||
// ── Footer STATE-A container id ──────────────────────────────────────────
|
||||
// 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row,
|
||||
// 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
|
||||
// 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
|
||||
// label widths (195px vs 145px in State B). _byId stores the LAST duplicate, which
|
||||
// is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
|
||||
// is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
|
||||
public const uint FooterStateAId = 0x10000240u; // State-A footer container (nothing selected)
|
||||
public const uint FooterStateBId = 0x10000241u; // State-B footer container (row selected)
|
||||
public const uint FooterStateCId = 0x10000247u; // State-C footer container (hide inactive)
|
||||
|
||||
// ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
|
||||
// ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
|
||||
// These are imported Type-12 UIElement_Text tabs. Their Closed/Open states carry
|
||||
// the caption color and PassToChildren=true; their three retained children carry
|
||||
// the authored left/center/right chrome. Character and Spellbook therefore share
|
||||
|
|
@ -89,7 +89,7 @@ internal static class CharacterStatController
|
|||
public const uint SkillsPageId = 0x1000022Cu;
|
||||
public const uint TitlesPageId = 0x10000539u;
|
||||
|
||||
// ── Footer element ids (gmStatManagementUI struct fields) ────────────────
|
||||
// ── Footer element ids (gmStatManagementUI struct fields) ────────────────
|
||||
// Source: acclient.h / DisplayDefaultFooter (0x0049cde0)
|
||||
public const uint FooterTitleId = 0x1000024eu; // GetFooterTitleLabel
|
||||
public const uint FooterLine1Label = 0x10000242u; // GetFooterLineOneLabel
|
||||
|
|
@ -97,26 +97,26 @@ internal static class CharacterStatController
|
|||
public const uint FooterLine2Label = 0x10000244u; // GetFooterLineTwoLabel
|
||||
public const uint FooterLine2Value = 0x10000245u; // GetFooterLineTwoValue
|
||||
|
||||
// ── Raise button element ids ──────────────────────────────────────────────
|
||||
// ── Raise button element ids ──────────────────────────────────────────────
|
||||
// Source: gmAttributeUI::PostInit (0x0049db70); CM_Train::Event_TrainAttribute.
|
||||
// Button state "Normal" (UIStateId 0x01) = affordable (green/active);
|
||||
// "Ghosted" (UIStateId 0x0D) = disabled. Hidden when nothing is selected.
|
||||
public const uint RaiseOneId = 0x10000246u; // raise × 1
|
||||
public const uint RaiseTenId = 0x100005EBu; // raise × 10
|
||||
public const uint RaiseOneId = 0x10000246u; // raise × 1
|
||||
public const uint RaiseTenId = 0x100005EBu; // raise × 10
|
||||
|
||||
private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text
|
||||
private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold
|
||||
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.</summary>
|
||||
private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f);
|
||||
private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f);
|
||||
|
||||
// ── Row layout constants ─────────────────────────────────────────────────
|
||||
// ── Row layout constants ─────────────────────────────────────────────────
|
||||
// RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height
|
||||
// and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side.
|
||||
// The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which
|
||||
// gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
|
||||
// gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
|
||||
private const float RowHeight = 22f;
|
||||
private const float IconSize = 16f;
|
||||
private const float RowPadX = 4f;
|
||||
|
|
@ -146,7 +146,7 @@ internal static class CharacterStatController
|
|||
Skills,
|
||||
}
|
||||
|
||||
internal enum RaiseTargetKind
|
||||
public enum RaiseTargetKind
|
||||
{
|
||||
Attribute,
|
||||
Vital,
|
||||
|
|
@ -154,7 +154,7 @@ internal static class CharacterStatController
|
|||
TrainSkill,
|
||||
}
|
||||
|
||||
internal readonly record struct RaiseRequest(
|
||||
public readonly record struct RaiseRequest(
|
||||
RaiseTargetKind Kind,
|
||||
uint StatId,
|
||||
long Cost,
|
||||
|
|
@ -170,7 +170,7 @@ internal static class CharacterStatController
|
|||
|
||||
private sealed record SkillRowBinding(UiClickablePanel Panel, CharacterSkill Skill);
|
||||
|
||||
// ── Attribute row descriptors — retail display order per spec §1 ─────────
|
||||
// ── Attribute row descriptors — retail display order per spec §1 ─────────
|
||||
private static readonly (string name, uint iconDid, uint statId)[] AttrRows = new[]
|
||||
{
|
||||
("Strength", 0x060002C8u, 1u),
|
||||
|
|
@ -210,7 +210,7 @@ internal static class CharacterStatController
|
|||
Func<CharacterSheet> data,
|
||||
UiDatFont? datFont = null,
|
||||
UiDatFont? rowDatFont = null,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve = null,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve = null,
|
||||
RaiseRequestHandler? onRaiseRequest = null,
|
||||
Action? onClose = null)
|
||||
{
|
||||
|
|
@ -231,35 +231,35 @@ internal static class CharacterStatController
|
|||
UiElement? contentPage = FindDirectChildById(layout.Root, AttributesPageId);
|
||||
|
||||
// Name (18px from dat FontDid), Heritage (14px), PkStatus (14px):
|
||||
// Fix C: pass null → Label's null-guard keeps the build-time dat font.
|
||||
// Fix C: pass null → Label's null-guard keeps the build-time dat font.
|
||||
// Controllers still own the text color and the LinesProvider.
|
||||
// Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
|
||||
// Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
|
||||
Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name);
|
||||
Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data()));
|
||||
Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty);
|
||||
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
LabelTwoLine(layout, contentPage, LevelCaptionId, null, Body, "Character", "Level");
|
||||
|
||||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build
|
||||
// time when the font resolver is provided (studio path). We no longer force rowDatFont
|
||||
// here for the level — the dat's own FontDid drives the font. The Gold color is still
|
||||
// here for the level — the dat's own FontDid drives the font. The Gold color is still
|
||||
// set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in
|
||||
// BuildAttributeRows) continue to use datFont directly since they have no dat origin.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString());
|
||||
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):");
|
||||
LabelRight(layout, contentPage, TotalXpId, null, Body, () => data().TotalXp.ToString("N0"));
|
||||
|
||||
// XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70).
|
||||
// Fix 5: child elements 0x10000237 (label) and 0x10000238 (value) are now built by
|
||||
// the LayoutImporter as UiText children of the XP meter (non-Type-3 meter children
|
||||
// are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
|
||||
// are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
|
||||
// FindElement now returns them; the controller binds their LinesProvider.
|
||||
// The importer builds them as UiText via DatWidgetFactory.BuildText, applying their
|
||||
// dat-origin HJustify/VJustify/FontDid/FontColor at build time. The controller then
|
||||
|
|
@ -272,21 +272,21 @@ internal static class CharacterStatController
|
|||
// Bind the dat-origin XP label (0x10000237) and value (0x10000238).
|
||||
// These are now real UiText children of the meter (built by the importer).
|
||||
// The retail layout places the caption + value ON TOP of the red bar
|
||||
// (ref 2026-06-26: "value … with the red fill bar behind it").
|
||||
// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
|
||||
// (ref 2026-06-26: "value … with the red fill bar behind it").
|
||||
// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
|
||||
if (FindTextByDatId(layout, contentPage, XpNextLabelId) is UiText xpLabel)
|
||||
{
|
||||
if (datFont is not null) xpLabel.DatFont = datFont;
|
||||
xpLabel.ClickThrough = true;
|
||||
xpLabel.Centered = false; // left-align (retail: aligns with Total XP label above)
|
||||
xpLabel.RightAligned = false;
|
||||
xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
|
||||
xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
|
||||
|
||||
// Item 1: align the XP-next label's left edge to match the TotalXpLabel's
|
||||
// absolute left edge. The XP-next label is a child of the meter (local coords),
|
||||
// so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
|
||||
// so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
|
||||
// horizontal offset within the panel (the meter starts to the right of the
|
||||
// "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
|
||||
// "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
|
||||
// "XP for next level:" caption left-aligns with "Total Experience (XP):" above).
|
||||
if (FindElementByDatId(layout, contentPage, TotalXpLabelId) is { } totalXpLbl)
|
||||
{
|
||||
|
|
@ -310,7 +310,7 @@ internal static class CharacterStatController
|
|||
// The tab visuals are already retained in the imported LayoutDesc. Controllers
|
||||
// bind only click behavior and the active Open/Closed state below.
|
||||
|
||||
// ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
|
||||
// ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
|
||||
// Mutable selected-index box: -1 = nothing selected.
|
||||
|
||||
// Gather EVERY copy of the raise buttons in the tree. The raise button ids
|
||||
|
|
@ -322,7 +322,7 @@ internal static class CharacterStatController
|
|||
// At bind time the tree includes all three tab pages (the page-visibility pass
|
||||
// runs AFTER this). Collecting from the full tree is safe: once the page-
|
||||
// visibility pass hides the inactive pages their raise buttons are invisible
|
||||
// regardless of the Visible flag we set here — but the Attributes page's
|
||||
// regardless of the Visible flag we set here — but the Attributes page's
|
||||
// buttons (which are NOT hidden by the page pass) must be explicitly hidden.
|
||||
var allRaise1 = new List<UiButton>();
|
||||
var allRaise10 = new List<UiButton>();
|
||||
|
|
@ -365,7 +365,7 @@ internal static class CharacterStatController
|
|||
foreach (var b in allRaise1) b.Visible = false;
|
||||
foreach (var b in allRaise10) b.Visible = false;
|
||||
|
||||
// ── Footer state visibility ───────────────────────────────────────────
|
||||
// ── Footer state visibility ───────────────────────────────────────────
|
||||
// There are THREE footer state groups (A=0x10000240, B=0x10000241, C=0x10000247)
|
||||
// all stacked at the same position within the Attributes page. _byId stores only
|
||||
// the LAST copy of each id; the others live in the VISIBLE Attributes page and must
|
||||
|
|
@ -373,13 +373,13 @@ internal static class CharacterStatController
|
|||
//
|
||||
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
|
||||
// All three group elements have DefaultState = StatManagement_Footer_Default
|
||||
// (0x10000011) — the dat does NOT differentiate them by visibility. The parent
|
||||
// (0x10000011) — the dat does NOT differentiate them by visibility. The parent
|
||||
// element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren=
|
||||
// true, but each child group also registers all three states (IncFlags=None,
|
||||
// Media=0) — meaning the state-propagation produces no media change on any group.
|
||||
// Media=0) — meaning the state-propagation produces no media change on any group.
|
||||
// Retail's gmStatManagementUI uses hardcoded element-id dispatch
|
||||
// (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247
|
||||
// for Meter) to access the right group's children at runtime — the groups themselves
|
||||
// for Meter) to access the right group's children at runtime — the groups themselves
|
||||
// are never hidden/shown via the dat state mechanism. The controller is the correct
|
||||
// and only place for this visibility management. See retail decomp
|
||||
// gmStatManagementUI::GetFooterTitleLabel @0x004f0170.
|
||||
|
|
@ -389,7 +389,7 @@ internal static class CharacterStatController
|
|||
// selected and owns the retail raise buttons; State C stays hidden for now.
|
||||
SetFooterSelected(false);
|
||||
|
||||
// ── Footer State A initial binding ────────────────────────────────────
|
||||
// ── Footer State A initial binding ────────────────────────────────────
|
||||
// Walk to the State-A container directly (rather than _byId which returns the
|
||||
// last duplicate) so we get the wider-label copies (195px) for the unselected state.
|
||||
BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel, contentPage);
|
||||
|
|
@ -428,10 +428,10 @@ internal static class CharacterStatController
|
|||
RetailTabBinding.SetClick(titlesTab, null);
|
||||
UpdateTabStates();
|
||||
|
||||
// ── Active-page selection (fixes the dark-overlay) ─────────────────────
|
||||
// ── Active-page selection (fixes the dark-overlay) ─────────────────────
|
||||
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
|
||||
// The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills,
|
||||
// 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
|
||||
// 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
|
||||
// visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI
|
||||
// via SetVisible(bool) on the page containers. The controller is the correct
|
||||
// and only place for initial tab-page selection.
|
||||
|
|
@ -565,7 +565,7 @@ internal static class CharacterStatController
|
|||
ImportedLayout layout,
|
||||
UiElement? contentPage,
|
||||
UiElement? statList,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve)
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve)
|
||||
{
|
||||
if (spriteResolve is null)
|
||||
return null;
|
||||
|
|
@ -624,7 +624,7 @@ internal static class CharacterStatController
|
|||
|
||||
private static void ConfigureSkillScrollbar(
|
||||
UiScrollbar bar,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)> spriteResolve)
|
||||
Func<uint, (uint handle, int w, int h)> spriteResolve)
|
||||
{
|
||||
bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); };
|
||||
bar.TrackSprite = ScrollTrackSprite;
|
||||
|
|
@ -649,12 +649,12 @@ internal static class CharacterStatController
|
|||
: SkillContentWidth;
|
||||
}
|
||||
|
||||
// ── 9-row attribute list ─────────────────────────────────────────────────
|
||||
// ── 9-row attribute list ─────────────────────────────────────────────────
|
||||
|
||||
private static List<UiClickablePanel> BuildAttributeRows(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
int[] sel,
|
||||
List<UiButton> allRaise1,
|
||||
|
|
@ -736,7 +736,7 @@ internal static class CharacterStatController
|
|||
private static List<UiElement> BuildSkillRows(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
int[] sel,
|
||||
List<UiButton> allRaise1,
|
||||
|
|
@ -792,7 +792,7 @@ internal static class CharacterStatController
|
|||
private static UiPanel AddSkillHeader(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
float left,
|
||||
float top,
|
||||
float width,
|
||||
|
|
@ -878,14 +878,14 @@ internal static class CharacterStatController
|
|||
: Vector4.One;
|
||||
|
||||
/// <summary>
|
||||
/// Handles a row click: toggle (same row → deselect), else select new row.
|
||||
/// Handles a row click: toggle (same row → deselect), else select new row.
|
||||
/// Updates highlight, footer providers, and raise-button state.
|
||||
/// </summary>
|
||||
private static void HandleRowClick(
|
||||
int clickedIndex,
|
||||
int[] sel,
|
||||
List<UiClickablePanel> rows,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
List<UiButton> allRaise1,
|
||||
List<UiButton> allRaise10)
|
||||
|
|
@ -895,10 +895,10 @@ internal static class CharacterStatController
|
|||
|
||||
// Log for live test confirmation (user tests selection in the studio).
|
||||
string rowName = GetRowName(newSel);
|
||||
Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
|
||||
Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
|
||||
|
||||
// Update highlight on all rows.
|
||||
// Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
|
||||
// Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
|
||||
// for the selected row background. When spriteResolve is available, apply the
|
||||
// sprite; otherwise fall back to the translucent gold tint.
|
||||
const uint HighlightSprite = 0x06001397u;
|
||||
|
|
@ -936,7 +936,7 @@ internal static class CharacterStatController
|
|||
int clickedIndex,
|
||||
int[] sel,
|
||||
List<SkillRowBinding> rows,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
List<UiButton> allRaise1,
|
||||
List<UiButton> allRaise10)
|
||||
|
|
@ -956,7 +956,7 @@ internal static class CharacterStatController
|
|||
private static void ApplySkillSelectionVisuals(
|
||||
int selectedIndex,
|
||||
IReadOnlyList<SkillRowBinding> rows,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve)
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve)
|
||||
{
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
|
|
@ -1227,7 +1227,7 @@ internal static class CharacterStatController
|
|||
private static UiClickablePanel AddRow(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
float left, float top, float width, float height,
|
||||
uint iconDid,
|
||||
string nameText,
|
||||
|
|
@ -1317,7 +1317,7 @@ internal static class CharacterStatController
|
|||
return row;
|
||||
}
|
||||
|
||||
// ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
|
||||
// ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Bind all 5 footer elements with providers that close over <paramref name="sel"/>:
|
||||
|
|
@ -1361,7 +1361,7 @@ internal static class CharacterStatController
|
|||
// IMPORTANT: The footer state id (0x10000240) appears once per tab-page sub-layout
|
||||
// (Attributes / Skills / Titles). layout._byId stores only the LAST registered copy,
|
||||
// which ends up in the LAST-imported tab page (Titles). The page-visibility pass
|
||||
// hides the Titles page → the bound footer elements would be invisible.
|
||||
// hides the Titles page → the bound footer elements would be invisible.
|
||||
//
|
||||
// Fix: find State A/B inside the explicit Attributes page, not via _byId. The
|
||||
// id dictionary stores the last duplicate and can point at a hidden Skills/Titles
|
||||
|
|
@ -1401,19 +1401,19 @@ internal static class CharacterStatController
|
|||
// The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so
|
||||
// without an override the text would center vertically in the 55px box, overlapping
|
||||
// line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the
|
||||
// top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
|
||||
// The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
|
||||
// top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
|
||||
// The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
|
||||
// BackgroundSprite cleared: its full-height sprite would cover line-1/line-2.
|
||||
titleEl.BackgroundSprite = 0;
|
||||
titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above)
|
||||
titleEl.OneLine = true;
|
||||
}
|
||||
// Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
|
||||
// Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
|
||||
// Fix C: the dat has a 20px font for the footer title. Let it drive.
|
||||
if (titleEl is not null)
|
||||
{
|
||||
// DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// RightAligned stays false (BuildText default for a Center element).
|
||||
titleEl.ClickThrough = true;
|
||||
titleEl.LinesProvider = () =>
|
||||
|
|
@ -1440,7 +1440,7 @@ internal static class CharacterStatController
|
|||
};
|
||||
}
|
||||
|
||||
// Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
|
||||
// Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
|
||||
var l1L = ByPos(20f, 5f, FooterLine1Label);
|
||||
LabelProvider(l1L, null, Body, () =>
|
||||
{
|
||||
|
|
@ -1475,7 +1475,7 @@ internal static class CharacterStatController
|
|||
return cost > 0 ? cost.ToString("N0") : "Infinity!";
|
||||
});
|
||||
|
||||
// Line-2 elements: pass null → keep dat font.
|
||||
// Line-2 elements: pass null → keep dat font.
|
||||
var l2L = ByPos(37f, 5f, FooterLine2Label);
|
||||
LabelProvider(l2L, null, Body, () =>
|
||||
{
|
||||
|
|
@ -1596,7 +1596,7 @@ internal static class CharacterStatController
|
|||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static void SetCompatibilityAnchorsAllById(
|
||||
UiElement node,
|
||||
|
|
@ -1707,7 +1707,7 @@ internal static class CharacterStatController
|
|||
/// <see cref="UiText"/> renders multiple lines oldest-first (top-to-bottom), so
|
||||
/// line 0 = <paramref name="line1"/> (top) and line 1 = <paramref name="line2"/> (bottom).
|
||||
/// This replaces the single-line "Character Level" caption which truncated in the 65px element.
|
||||
/// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).</summary>
|
||||
/// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).</summary>
|
||||
private static void LabelTwoLine(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color,
|
||||
string line1, string line2)
|
||||
=> LabelTwoLine(layout, null, id, datFont, color, line1, line2);
|
||||
|
|
@ -1719,7 +1719,7 @@ internal static class CharacterStatController
|
|||
{
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = false; // non-Centered → scroll/multi-line path
|
||||
t.Centered = false; // non-Centered → scroll/multi-line path
|
||||
t.RightAligned = false;
|
||||
t.ClickThrough = true;
|
||||
t.Padding = 1f;
|
||||
|
|
@ -1732,7 +1732,7 @@ internal static class CharacterStatController
|
|||
}
|
||||
|
||||
/// <summary>Left-justified label (for captions that should be left-aligned, not centered).
|
||||
/// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
|
||||
/// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
|
||||
/// being clipped by the bottom-pin scroll math (top=Padding, bottom=H-Padding).</summary>
|
||||
private static void LabelLeft(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
=> LabelLeft(layout, null, id, datFont, color, text);
|
||||
|
|
@ -1776,8 +1776,8 @@ internal static class CharacterStatController
|
|||
/// <summary>Bind a directly-located <see cref="UiText"/> widget with a provider.
|
||||
/// Used when the widget was found by subtree walk rather than <c>FindElement</c>.
|
||||
/// Sets <c>Padding = 0</c> to prevent the scroll-clip from hiding text in small
|
||||
/// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
|
||||
/// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.</summary>
|
||||
/// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
|
||||
/// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.</summary>
|
||||
private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
{
|
||||
if (t is null) return;
|
||||
|
|
@ -1798,7 +1798,7 @@ internal static class CharacterStatController
|
|||
/// The standard <see cref="ImportedLayout.FindElement"/> returns only the LAST widget
|
||||
/// registered for a given id; for elements duplicated across tab-page sub-layouts
|
||||
/// (raise buttons, close buttons) we need ALL copies so that visibility changes are
|
||||
/// reflected in every page — not just the last-mounted one.
|
||||
/// reflected in every page — not just the last-mounted one.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -1810,14 +1810,14 @@ internal static class CharacterStatController
|
|||
/// We therefore walk the tree recursively and collect every <see cref="UiButton"/> whose
|
||||
/// <c>ActiveState</c> reflects the dat default (before our code sets it), which is not a
|
||||
/// reliable discriminator. Instead, we gather ALL <see cref="UiButton"/> instances from
|
||||
/// the subtree at the known spatial position (bottom of the panel) — but positions can
|
||||
/// the subtree at the known spatial position (bottom of the panel) — but positions can
|
||||
/// overlap across pages.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The correct approach: since <c>_byId</c> stores only one instance per id, we use the
|
||||
/// <see cref="ImportedLayout.FindElement"/> for the canonical id, then do a FULL tree walk
|
||||
/// to find ADDITIONAL <see cref="UiButton"/> instances that have identical Width×Height to
|
||||
/// to find ADDITIONAL <see cref="UiButton"/> instances that have identical Width×Height to
|
||||
/// the known button. This works because the three page copies share the same dat template
|
||||
/// and thus the same geometry. Collected via reference-equality guard to avoid duplicates.
|
||||
/// </para>
|
||||
|
|
@ -1833,7 +1833,7 @@ internal static class CharacterStatController
|
|||
_ = layout;
|
||||
|
||||
// Walk the tree and collect ALL UiButton instances matching the canonical geometry.
|
||||
// The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
|
||||
// The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
|
||||
var seen = new HashSet<UiButton>(ReferenceEqualityComparer.Instance);
|
||||
CollectMatchingButtons(node, targetId, seen, result);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue