fix(ui): Campaign AS gate fixes AS-GF1 — extras-list overflow ruled OUT as a code defect; paperdoll regression not isolated, probe added instead
Two owner-reported defects at the Campaign AS connected gate on the examination window (player targets): the animated paperdoll no longer renders at all, and a "reserved black rectangle" appears at the window's bottom with the character extras list clipped mid-row at default (310x400) window size. ROOT CAUSE — extras-list overflow (the "clipped mid-row" half of defect 2): NOT a code bug. AS3 (armor-level trio) and AS4 (society/allegiance/ configurable extras) grew the extras list past its DAT-authored 87px region (element 0x10000335) at the window's minimum size — a new hermetic regression test proves the worst-case combination (every AS3+AS4 addition at once) reaches 20 rows / 400px of content, a 4.6x overflow. But retail's own LayoutDesc authors NO scrollbar for this listbox either (ScrollbarElementId == 0, verified against both the committed fixture and a fresh tools/LayoutDump read of the live installed DAT — no drift), and the SAME test proves UiItemList's pre-existing, unmodified wheel-scroll handler (OnEvent's UiEventType.Scroll branch) already reveals every row on the next paint. A scrollbar-less, wheel-scrollable list clipped to its authored region until the user scrolls or resizes IS retail's own already-correctly- ported mechanism, not a regression — so no fix was made here. ROOT CAUSE — paperdoll / "black rectangle" (defect 1): NOT ISOLATED despite exhaustive investigation. Every file the Campaign AS diff touches (AppraisalUiController.cs, RetailUiRuntime.cs, CreatureAppraisalRows.cs, AllegianceRankTitleTable.cs, CharacterIdentityText.cs, CharacterSheetProvider.cs, InteractionRetainedUiComposition.cs, plus two unrelated mechanical PublicWeenieFlags-literal refactors) was reviewed in full against the pre-Campaign-AS baseline. The same worst-case regression test proves Apply/ApplyCreature/RebuildCreatureStats/BuildExtra never throw and always leave ActiveView == Character, CurrentObjectId != 0, and the viewport's full ancestor-visibility chain Visible == true — ruling out RetailCreatureAppraisalFrameView.TryGetVisibleTarget's first three gates. CreatureAppraisalPresentation.cs and LivePresentationComposition.cs (the entire render-time viewport pipeline) are byte-for-byte unchanged across the whole 974fe88a..87e98395 window. UiViewport.OnDraw draws NOTHING (not black) when its TextureSlot is unassigned, and the creaturePanel's own full-panel backdrop (0x10000141) is what would show through instead — the most likely explanation tying both defects to ONE underlying condition, but its exact trigger (TryGetVisibleTarget's CurrentObjectId check, or TrySynchronize's live-entity/mesh-availability check) lies in code nothing in Campaign AS touches, and could not be reproduced hermetically (needs a live entity + a live examine exchange). Filed #443 with the full investigation trail. Added a temporary, state-change-gated diagnostic probe (ACDREAM_PROBE_CREATURE_APPRAISAL_ VIEWPORT=1, CreatureAppraisalViewportDiagnostics) at both TryGetVisibleTarget and TrySynchronize so the next live repro pinpoints the exact failing reason instead of another guess. Per CLAUDE.md's "no workarounds without explicit approval" and the investigation mode's own escape hatch ("if you cannot root-cause, say what runtime evidence you need instead of shipping a guess"), no behavioral fix was shipped for defect 1. Tests: AcDream.App.Tests hermetic filter 6,337/0; full-solution hermetic suite 15,612/0 (all 14 projects green, including the known #442 flake, which did not trip this run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
87e9839561
commit
65f6f5848a
5 changed files with 302 additions and 7 deletions
|
|
@ -1153,6 +1153,134 @@ public sealed class AppraisalUiControllerTests
|
|||
("Society:", "Celestial Hand"), ExtraRow(extra, 0));
|
||||
}
|
||||
|
||||
// ── AS-GF1 diagnostic: DEFECT-1/DEFECT-2 root-cause probe ──────────────
|
||||
// Combines EVERY AS3+AS4 extras-list addition on one response (armor
|
||||
// levels, society, allegiance cascade, ratings, all seven configurable
|
||||
// extras) through the REAL examination layout + REAL row templates —
|
||||
// the worst-case content length none of the individual AS3/AS4 tests
|
||||
// exercise together. Confirms (a) no exception anywhere in
|
||||
// Apply/ApplyCreature/RebuildCreatureStats/BuildExtra for this
|
||||
// combination, (b) the viewport's own visibility gate
|
||||
// (RetailCreatureAppraisalFrameView.TryGetVisibleTarget's ActiveView/
|
||||
// CurrentObjectId/ancestor-visible conditions) is unaffected by extras-
|
||||
// list length, and (c) measures the real overflow magnitude driving
|
||||
// DEFECT 2.
|
||||
[Fact]
|
||||
public void CharacterResponse_WorstCaseExtrasCombination_DoesNotThrowAndViewportGateStaysOpen()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = ObjectId,
|
||||
Name = "Worstcase",
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
using var interaction = NewInteraction(objects, []);
|
||||
var templates = new CreatureAppraisalRowTemplateFactory(
|
||||
FixtureLoader.LoadExaminationRowTemplateInfos(),
|
||||
NoTexture,
|
||||
defaultFont: null);
|
||||
using AppraisalUiController controller = Bind(
|
||||
layout,
|
||||
objects,
|
||||
interaction,
|
||||
new CombatState(),
|
||||
[],
|
||||
[],
|
||||
() => { },
|
||||
() => { },
|
||||
templates,
|
||||
resolveCharacterTitle: titleId => titleId == 13u ? "War Mage" : null,
|
||||
localFactionBits: () => 0x1)!;
|
||||
|
||||
interaction.ExamineSelectedOrEnterMode(ObjectId);
|
||||
var properties = new PropertyBundle();
|
||||
properties.Strings[5u] = "Template"; // Character-view marker
|
||||
properties.Ints[0x105] = 13; // CharacterTitleId
|
||||
properties.Ints[113] = 1; // Gender: male
|
||||
properties.Ints[188] = 1; // HeritageGroup: Aluvian
|
||||
properties.Ints[281] = 0x1; // Faction1Bits: Celestial Hand
|
||||
properties.Ints[287] = 50; // Society rank
|
||||
properties.Ints[30] = 5; // AllegianceRank >= 1
|
||||
properties.Ints[35] = 12; // AllegianceFollowers (unused once titles present)
|
||||
properties.Strings[21u] = "Monarch Title";
|
||||
properties.Strings[35u] = "Patron Title"; // different from Monarch -> two rows
|
||||
properties.Ints[0x133] = 10; // DamageRating
|
||||
properties.Ints[0x134] = 10; // DamageResistRating
|
||||
properties.Ints[0x15E] = 10; // DotResistRating
|
||||
properties.Strings[10u] = "Fellows";
|
||||
properties.Strings[43u] = "1/1/2003"; // DateOfBirth
|
||||
properties.Ints[125] = 100000; // Age (seconds in Dereth)
|
||||
properties.Ints[181] = 7; // ChessRank
|
||||
properties.Ints[192] = 3; // FishingSkill
|
||||
properties.Ints[43u] = 2; // NumDeaths (int table, same numeric id as DateOfBirth string id)
|
||||
properties.Ints[262] = 5; // NumCharacterTitles
|
||||
var armorLevels = new AppraiseInfoParser.ArmorLevel(
|
||||
Head: 100, Chest: 110, Abdomen: 120,
|
||||
UpperArm: 130, LowerArm: 140, Hand: 150,
|
||||
UpperLeg: 160, LowerLeg: 170, Foot: 180);
|
||||
|
||||
bool applied = controller.Apply(Parsed(
|
||||
properties, MinimalCreatureProfile(), armorLevels: armorLevels));
|
||||
|
||||
Assert.True(applied);
|
||||
Assert.Equal(AppraisalView.Character, controller.ActiveView);
|
||||
Assert.NotEqual(0u, controller.CurrentObjectId);
|
||||
|
||||
// The viewport-visibility gate this test exists to protect:
|
||||
// RetailCreatureAppraisalFrameView.TryGetVisibleTarget requires the
|
||||
// creaturePanel (viewport's ancestor) to report Visible, exactly
|
||||
// like SetActiveView's `_creaturePanel.Visible = view is Creature or
|
||||
// Character` line sets it.
|
||||
UiElement creaturePanel = layout.FindElement(
|
||||
AppraisalUiController.CreaturePanelId)!;
|
||||
UiElement viewportHost = layout.FindElement(
|
||||
AppraisalUiController.CreatureViewportId)!;
|
||||
Assert.True(creaturePanel.Visible);
|
||||
for (UiElement? current = viewportHost; current is not null; current = current.Parent)
|
||||
Assert.True(current.Visible, $"ancestor 0x{current.EventId:X8} is not Visible");
|
||||
|
||||
UiItemList extra = CreatureExtraList(layout);
|
||||
int rowCount = extra.GetNumUIItems();
|
||||
UiElement extraHost = layout.FindElement(
|
||||
AppraisalUiController.CreatureExtraListId)!;
|
||||
float contentHeight = rowCount * 20f; // CreatureAppraisalLayeredList.NewList's CellHeight
|
||||
Console.WriteLine(
|
||||
$"[AS-GF1] worst-case extras: {rowCount} rows, "
|
||||
+ $"{contentHeight}px content vs extraHost authored "
|
||||
+ $"{extraHost.Height}px at default window size.");
|
||||
|
||||
// DEFECT 2's measured overflow: AS3+AS4's combined worst case is
|
||||
// dramatically taller than the DAT-authored 87px region at the
|
||||
// window's minimum (310x400) size.
|
||||
Assert.True(rowCount > 15, $"expected a long worst-case list, got {rowCount} rows");
|
||||
Assert.True(contentHeight > extraHost.Height * 2,
|
||||
$"expected content ({contentHeight}px) to badly overflow the "
|
||||
+ $"authored host ({extraHost.Height}px)");
|
||||
|
||||
// AS-GF1 probe: does UiItemList's generic wheel-scroll handler
|
||||
// (OnEvent's UiEventType.Scroll branch, gated on CellWidth > 0f,
|
||||
// which NewList sets) actually reveal the rows below the fold, the
|
||||
// same way it does for every other scrollable list in this
|
||||
// controller? If so, the overflow is inert (retail's own DAT
|
||||
// authors NO ScrollbarElementId for either 0x10000149 or 0x10000335
|
||||
// either — verified against both the committed fixture and a fresh
|
||||
// `tools/LayoutDump 0x2100006B 0x10000140 --props` read of the live
|
||||
// installed DAT) and NOT itself a code defect.
|
||||
UiItemSlot lastRow = Assert.IsType<UiTemplateListSlot>(
|
||||
extra.GetItem(rowCount - 1));
|
||||
Assert.False(lastRow.Visible, "expected the last row to start below the fold");
|
||||
extra.OnEvent(new UiEvent(
|
||||
extra.EventId, extra, UiEventType.Scroll, Data0: -1000));
|
||||
Assert.True(extra.Scroll.ScrollY > 0, "expected the wheel event to move the scroll offset");
|
||||
// OnEvent only moves the shared Scroll's offset; cell.Visible/Top are
|
||||
// only recomputed by LayoutCells(), which OnDraw calls every frame.
|
||||
// Simulate the next paint (this hermetic test never renders one).
|
||||
extra.LayoutCells();
|
||||
Assert.True(lastRow.Visible, "expected scrolling to reveal the last row");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatureResponse_NeverGainsArmorLevelTrioOrLegend()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue