feat(ui): port retained widget foundations
This commit is contained in:
parent
44f9ec13d9
commit
d825572e31
44 changed files with 84813 additions and 292 deletions
|
|
@ -1528,10 +1528,12 @@ public class CharacterStatControllerTests
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites).</summary>
|
||||
/// <summary>Manufacture a minimal UiButton with retail normal/ghosted states.</summary>
|
||||
private static UiButton MakeButton(uint id = 0u)
|
||||
{
|
||||
var info = new ElementInfo { Id = id, Type = 1 };
|
||||
info.StateMedia["Normal"] = (1u, 1);
|
||||
info.StateMedia["Ghosted"] = (2u, 1);
|
||||
return new UiButton(info, static _ => (0u, 0, 0));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -43,4 +44,18 @@ public class ChatLayoutConformanceTests
|
|||
Assert.Equal(12u, Find(root, 0x10000011u)!.Type); // Text/style-prototype (transcript)
|
||||
Assert.Equal(12u, Find(root, 0x10000016u)!.Type); // Text/style-prototype (input)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatFixture_BuildsSelectableTranscriptAndEditableInputInPlace()
|
||||
{
|
||||
var layout = FixtureLoader.LoadChat();
|
||||
|
||||
var transcript = Assert.IsType<UiText>(layout.FindElement(0x10000011u));
|
||||
var input = Assert.IsType<UiField>(layout.FindElement(0x10000016u));
|
||||
|
||||
Assert.True(transcript.Selectable);
|
||||
Assert.True(input.Selectable);
|
||||
Assert.True(input.OneLine);
|
||||
Assert.Equal(0x10000016u, input.DatElementId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
/// real chat layout hierarchy (root → transcript panel + input bar as Type-3
|
||||
/// containers, with Type-12 children for transcript + input, plus a Type-3 track
|
||||
/// and menu), call <see cref="LayoutImporter.Build"/> to get the widget tree
|
||||
/// (Type-12 children skipped, Type-3 parents created), then call
|
||||
/// <see cref="ChatWindowController.Bind"/> which reads rects from the info tree
|
||||
/// and places behavioral widgets under the parent containers.
|
||||
/// (Type-12 children become property-driven text/field widgets), then call
|
||||
/// <see cref="ChatWindowController.Bind"/> which binds those widgets in place.
|
||||
/// </summary>
|
||||
public class ChatWindowControllerTests
|
||||
{
|
||||
|
|
@ -42,7 +41,7 @@ public class ChatWindowControllerTests
|
|||
/// track (Type-3) [0x10000012] ← Type-3 in test (not Type-11); Bind skips scrollbar bind
|
||||
/// inputBar (Type-3) [0x10000013]
|
||||
/// menu (Type-6) [0x10000014]
|
||||
/// input (Type-12, no media) [0x10000016] ← built as UiText by factory; Bind removes + replaces with UiField
|
||||
/// input (Type-12, Editable+Selectable) [0x10000016] ← built as UiField
|
||||
/// send (Type-3) [0x10000019]
|
||||
/// maxmin (Type-3) [0x1000046F]
|
||||
/// </summary>
|
||||
|
|
@ -71,9 +70,26 @@ public class ChatWindowControllerTests
|
|||
};
|
||||
var inputNode = new ElementInfo
|
||||
{
|
||||
Id = 0x10000016u, Type = 12, // Type-12, no media → skipped by factory
|
||||
Id = 0x10000016u, Type = 12,
|
||||
X = 46, Y = 0, Width = 398, Height = 17,
|
||||
};
|
||||
var inputState = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
inputState.Properties.Values[0x16u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputState.Properties.Values[0x20u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputState.Properties.Values[0x27u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Bool,
|
||||
BoolValue = true,
|
||||
};
|
||||
inputNode.States[UiStateInfo.DirectStateId] = inputState;
|
||||
var sendNode = new ElementInfo
|
||||
{
|
||||
Id = 0x10000019u, Type = 3, X = 444, Y = 0, Width = 46, Height = 17,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,42 @@ public class DatWidgetFactoryTests
|
|||
public void Type12_Text_MakesUiText()
|
||||
{
|
||||
var e = DatWidgetFactory.Create(new ElementInfo { Type = 12, Width = 100, Height = 40 }, NoTex, null);
|
||||
Assert.IsType<UiText>(e);
|
||||
var text = Assert.IsType<UiText>(e);
|
||||
Assert.False(text.Selectable);
|
||||
Assert.True(text.ClickThrough);
|
||||
Assert.False(text.AcceptsFocus);
|
||||
Assert.False(text.CapturesPointerDrag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Type12_EditableProperty_MakesUiFieldInPlace()
|
||||
{
|
||||
var info = TextInfo(
|
||||
(0x16u, Bool(true)),
|
||||
(0x20u, Bool(true)),
|
||||
(0x27u, Bool(true)),
|
||||
(0x1Eu, Integer(80)));
|
||||
|
||||
var field = Assert.IsType<UiField>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
|
||||
Assert.Equal(info.Id, field.ElementId);
|
||||
Assert.True(field.OneLine);
|
||||
Assert.True(field.Selectable);
|
||||
Assert.Equal(80, field.MaxCharacters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Type12_SelectableProperty_MakesSelectableUiText()
|
||||
{
|
||||
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(
|
||||
TextInfo((0x27u, Bool(true))),
|
||||
NoTex,
|
||||
null));
|
||||
|
||||
Assert.True(text.Selectable);
|
||||
Assert.False(text.ClickThrough);
|
||||
Assert.True(text.AcceptsFocus);
|
||||
Assert.True(text.CapturesPointerDrag);
|
||||
}
|
||||
|
||||
// ── Test 4: Rect + anchors set from ElementInfo ───────────────────────────
|
||||
|
|
@ -74,6 +109,46 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, e.Anchors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportedDescendant_PreservesExactRawEdgePolicy()
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
Type = 3,
|
||||
X = 10,
|
||||
Y = 20,
|
||||
Width = 30,
|
||||
Height = 40,
|
||||
Left = 4,
|
||||
Top = 3,
|
||||
Right = 1,
|
||||
Bottom = 0,
|
||||
OriginalParentWidth = 100,
|
||||
OriginalParentHeight = 200,
|
||||
HasOriginalParentSize = true,
|
||||
};
|
||||
|
||||
var element = DatWidgetFactory.Create(info, NoTex, null)!;
|
||||
var policy = Assert.IsType<UiLayoutPolicy>(element.LayoutPolicy);
|
||||
|
||||
Assert.Equal(4u, policy.LeftMode);
|
||||
Assert.Equal(3u, policy.TopMode);
|
||||
Assert.Equal(1u, policy.RightMode);
|
||||
Assert.Equal(0u, policy.BottomMode);
|
||||
Assert.Equal(new UiPixelRect(0, 0, 99, 199), policy.OriginalParent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImportedRoot_HasNoRawEdgePolicy()
|
||||
{
|
||||
var element = DatWidgetFactory.Create(
|
||||
new ElementInfo { Type = 3, Width = 100, Height = 200 },
|
||||
NoTex,
|
||||
null)!;
|
||||
|
||||
Assert.Null(element.LayoutPolicy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_PropagatesStateCursors()
|
||||
{
|
||||
|
|
@ -339,6 +414,22 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(gold, t.DefaultColor);
|
||||
}
|
||||
|
||||
private static ElementInfo TextInfo(params (uint Id, UiPropertyValue Value)[] properties)
|
||||
{
|
||||
var info = new ElementInfo { Id = 0x10000016u, Type = 12, Width = 100, Height = 20 };
|
||||
var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
foreach (var property in properties)
|
||||
state.Properties.Values[property.Id] = property.Value;
|
||||
info.States[UiStateInfo.DirectStateId] = state;
|
||||
return info;
|
||||
}
|
||||
|
||||
private static UiPropertyValue Bool(bool value)
|
||||
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
||||
|
||||
private static UiPropertyValue Integer(int value)
|
||||
=> new() { Kind = UiPropertyKind.Integer, IntegerValue = value };
|
||||
|
||||
/// <summary>
|
||||
/// When ElementInfo.FontColor is null (dat carried no 0x1B), DefaultColor must
|
||||
/// stay at white (Vector4.One) — the backward-compatible default.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,30 @@ public static class FixtureLoader
|
|||
public static AcDream.App.UI.Layout.ElementInfo LoadRadarInfos()
|
||||
=> LoadInfos("radar_21000074.json");
|
||||
|
||||
public static ImportedLayout LoadToolbar()
|
||||
=> LayoutImporter.Build(LoadToolbarInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadToolbarInfos()
|
||||
=> LoadInfos("toolbar_21000016.json");
|
||||
|
||||
public static ImportedLayout LoadInventory()
|
||||
=> LayoutImporter.Build(LoadInventoryInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadInventoryInfos()
|
||||
=> LoadInfos("inventory_21000023.json");
|
||||
|
||||
public static ImportedLayout LoadPaperdoll()
|
||||
=> LayoutImporter.Build(LoadPaperdollInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadPaperdollInfos()
|
||||
=> LoadInfos("paperdoll_21000024.json");
|
||||
|
||||
public static ImportedLayout LoadCharacter()
|
||||
=> LayoutImporter.Build(LoadCharacterInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadCharacterInfos()
|
||||
=> LoadInfos("character_2100002E.json");
|
||||
|
||||
// ── Shared loader ────────────────────────────────────────────────────────
|
||||
|
||||
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,36 @@ public class LayoutConformanceTests
|
|||
Assert.Contains(0x40000000u, fontDids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VitalsBinding_ReusesAuthoredTextNodesWithoutDuplicates()
|
||||
{
|
||||
var layout = FixtureLoader.LoadVitals();
|
||||
VitalsController.Bind(
|
||||
layout,
|
||||
() => 1f,
|
||||
() => 1f,
|
||||
() => 1f,
|
||||
() => "100/100",
|
||||
() => "90/100",
|
||||
() => "80/100");
|
||||
|
||||
(uint MeterId, uint TextId)[] cases =
|
||||
[
|
||||
(VitalsController.Health, VitalsController.HealthText),
|
||||
(VitalsController.Stamina, VitalsController.StaminaText),
|
||||
(VitalsController.Mana, VitalsController.ManaText),
|
||||
];
|
||||
|
||||
foreach (var (meterId, textId) in cases)
|
||||
{
|
||||
var meter = Assert.IsType<UiMeter>(layout.FindElement(meterId));
|
||||
var text = Assert.IsType<UiText>(layout.FindElement(textId));
|
||||
Assert.Same(meter, text.Parent);
|
||||
Assert.Single(meter.Children, child => child == text);
|
||||
Assert.False(text.Selectable);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectFontDids(ElementInfo node, System.Collections.Generic.List<uint> acc)
|
||||
{
|
||||
if (node.FontDid != 0) acc.Add(node.FontDid);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class RetailFixtureConformanceTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(LayoutCases))]
|
||||
public void ProductionFixture_PreservesRootAndCanonicalStates(
|
||||
Func<ElementInfo> load,
|
||||
uint rootId,
|
||||
uint rootType,
|
||||
float width,
|
||||
float height,
|
||||
int minimumNodeCount)
|
||||
{
|
||||
var root = load();
|
||||
|
||||
Assert.Equal(rootId, root.Id);
|
||||
Assert.Equal(rootType, root.Type);
|
||||
Assert.Equal(width, root.Width);
|
||||
Assert.Equal(height, root.Height);
|
||||
Assert.True(CountNodes(root) >= minimumNodeCount);
|
||||
Assert.Contains(UiStateInfo.DirectStateId, root.States.Keys);
|
||||
Assert.Contains(Enumerate(root), element => element.States.Count > 1);
|
||||
Assert.Contains(Enumerate(root), element =>
|
||||
element.States.Values.Any(state => state.Properties.Values.Count > 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolbarFixture_ContainsObjectButtonsAndRetailButtonStates()
|
||||
{
|
||||
var root = FixtureLoader.LoadToolbarInfos();
|
||||
|
||||
var character = Find(root, 0x10000199u);
|
||||
var inventory = Find(root, 0x100001B1u);
|
||||
|
||||
Assert.NotNull(character);
|
||||
Assert.NotNull(inventory);
|
||||
Assert.Contains(1u, character.States.Keys); // Normal
|
||||
Assert.Contains(3u, character.States.Keys); // Normal_pressed
|
||||
Assert.Contains(6u, character.States.Keys); // Highlight
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryFixture_ContainsMountedBackpackAndPaperdollContent()
|
||||
{
|
||||
var root = FixtureLoader.LoadInventoryInfos();
|
||||
|
||||
Assert.NotNull(Find(root, InventoryController.ContentsGridId));
|
||||
Assert.NotNull(Find(root, InventoryController.ContainerListId));
|
||||
Assert.NotNull(Find(root, InventoryController.BurdenMeterId));
|
||||
Assert.NotNull(Find(root, 0x100001D5u)); // mounted paperdoll viewport
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterFixture_ContainsAllThreeRuntimePages()
|
||||
{
|
||||
var root = FixtureLoader.LoadCharacterInfos();
|
||||
|
||||
Assert.NotNull(Find(root, CharacterStatController.AttributesPageId));
|
||||
Assert.NotNull(Find(root, CharacterStatController.SkillsPageId));
|
||||
Assert.NotNull(Find(root, CharacterStatController.TitlesPageId));
|
||||
}
|
||||
|
||||
public static TheoryData<Func<ElementInfo>, uint, uint, float, float, int> LayoutCases
|
||||
=> new()
|
||||
{
|
||||
{ FixtureLoader.LoadToolbarInfos, 0x10000191u, 0x10000007u, 300f, 122f, 20 },
|
||||
{ FixtureLoader.LoadInventoryInfos, 0x100001CCu, 0x10000023u, 300f, 362f, 40 },
|
||||
{ FixtureLoader.LoadPaperdollInfos, 0x100001D4u, 0x10000024u, 224f, 214f, 20 },
|
||||
{ FixtureLoader.LoadCharacterInfos, 0x10000227u, 8u, 300f, 600f, 60 },
|
||||
};
|
||||
|
||||
private static int CountNodes(ElementInfo root)
|
||||
=> Enumerate(root).Count();
|
||||
|
||||
private static IEnumerable<ElementInfo> Enumerate(ElementInfo root)
|
||||
{
|
||||
yield return root;
|
||||
foreach (var child in root.Children)
|
||||
foreach (var descendant in Enumerate(child))
|
||||
yield return descendant;
|
||||
}
|
||||
|
||||
private static ElementInfo? Find(ElementInfo root, uint id)
|
||||
=> Enumerate(root).FirstOrDefault(element => element.Id == id);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates every committed retail layout fixture from production portal.dat.
|
||||
/// The test is inert unless <c>ACDREAM_REGENERATE_UI_FIXTURES=1</c> is set, keeping
|
||||
/// normal test runs deterministic and dat-independent.
|
||||
/// </summary>
|
||||
public sealed class RetailLayoutFixtureGenerator
|
||||
{
|
||||
private static readonly (uint Id, string FileName)[] Layouts =
|
||||
{
|
||||
(0x21000006u, "chat_21000006.json"),
|
||||
(0x21000016u, "toolbar_21000016.json"),
|
||||
(0x21000023u, "inventory_21000023.json"),
|
||||
(0x21000024u, "paperdoll_21000024.json"),
|
||||
(0x2100002Eu, "character_2100002E.json"),
|
||||
(0x2100006Cu, "vitals_2100006C.json"),
|
||||
(0x21000074u, "radar_21000074.json"),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void RegenerateAllRetailFixtures_WhenExplicitlyRequested()
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("ACDREAM_REGENERATE_UI_FIXTURES"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
foreach (var (layoutId, fileName) in Layouts)
|
||||
{
|
||||
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
||||
Assert.NotNull(info);
|
||||
var json = JsonSerializer.Serialize(info, new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true,
|
||||
WriteIndented = true,
|
||||
});
|
||||
File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), json);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixtureDirectory([CallerFilePath] string thisFile = "")
|
||||
=> Path.Combine(Path.GetDirectoryName(thisFile)!, "fixtures");
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
|
|
@ -87,4 +88,42 @@ public class UiDatElementTests
|
|||
// No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState).
|
||||
Assert.Equal(0x06007777u, e.ActiveMedia().File);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumericStateBridge_SetsNamedStateWithoutChangingGeometry()
|
||||
{
|
||||
var info = new ElementInfo { X = 4, Y = 5, Width = 30, Height = 40 };
|
||||
info.States[RetailUiStateIds.ShowDetail] = new UiStateInfo
|
||||
{
|
||||
Id = RetailUiStateIds.ShowDetail,
|
||||
Name = "ShowDetail",
|
||||
Image = new UiImageMedia(0x06000009u, 3),
|
||||
};
|
||||
info.StateMedia["ShowDetail"] = (0x06000009u, 3);
|
||||
var element = new UiDatElement(info, _ => (0u, 0, 0))
|
||||
{
|
||||
Left = info.X,
|
||||
Top = info.Y,
|
||||
Width = info.Width,
|
||||
Height = info.Height,
|
||||
};
|
||||
|
||||
Assert.True(element.TrySetRetailState(RetailUiStateIds.ShowDetail));
|
||||
|
||||
Assert.Equal(RetailUiStateIds.ShowDetail, element.ActiveRetailStateId);
|
||||
Assert.Equal((0x06000009u, 3), element.ActiveMedia());
|
||||
Assert.Equal((4f, 5f, 30f, 40f),
|
||||
(element.Left, element.Top, element.Width, element.Height));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumericStateBridge_MissingStatePreservesCurrentState()
|
||||
{
|
||||
var info = new ElementInfo { DefaultStateName = "Normal" };
|
||||
info.StateMedia["Normal"] = (0x06000001u, 1);
|
||||
var element = new UiDatElement(info, _ => (0u, 0, 0));
|
||||
|
||||
Assert.False(element.TrySetRetailState(RetailUiStateIds.ShowDetail));
|
||||
Assert.Equal("Normal", element.ActiveState);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
138
tests/AcDream.App.Tests/UI/Layout/UiPropertyBagTests.cs
Normal file
138
tests/AcDream.App.Tests/UI/Layout/UiPropertyBagTests.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI.Layout;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class UiPropertyBagTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_UsesKeyPresence_ForExplicitFalseAndZero()
|
||||
{
|
||||
var baseBag = new UiPropertyBag();
|
||||
baseBag.Values[0x16u] = Bool(true);
|
||||
baseBag.Values[0x14u] = Enum(3u);
|
||||
|
||||
var derivedBag = new UiPropertyBag();
|
||||
derivedBag.Values[0x16u] = Bool(false);
|
||||
derivedBag.Values[0x14u] = Enum(0u);
|
||||
|
||||
var merged = UiPropertyBag.Merge(baseBag, derivedBag);
|
||||
|
||||
Assert.False(merged.Values[0x16u].BoolValue);
|
||||
Assert.Equal(0ul, merged.Values[0x14u].UnsignedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveProperty_NamedStateOverridesDirectStateByPresence()
|
||||
{
|
||||
var info = new ElementInfo { DefaultStateId = 1u };
|
||||
info.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x16u, Bool(true)));
|
||||
info.States[1u] = State(1u, "Normal", (0x16u, Bool(false)));
|
||||
|
||||
Assert.True(info.TryGetEffectiveProperty(0x16u, out var property));
|
||||
Assert.False(property.BoolValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElementMerge_ExplicitCenteredStatePropertyOverridesBaseLeft()
|
||||
{
|
||||
var baseInfo = new ElementInfo();
|
||||
baseInfo.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x14u, Enum(0u)));
|
||||
|
||||
var derivedInfo = new ElementInfo();
|
||||
derivedInfo.States[UiStateInfo.DirectStateId] = State(
|
||||
UiStateInfo.DirectStateId,
|
||||
"",
|
||||
(0x14u, Enum(1u)));
|
||||
|
||||
var merged = ElementReader.Merge(baseInfo, derivedInfo);
|
||||
|
||||
Assert.Equal(HJustify.Center, merged.HJustify);
|
||||
Assert.Equal(1ul, merged.States[UiStateInfo.DirectStateId]
|
||||
.Properties.Values[0x14u].UnsignedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertProperty_PreservesNestedRetailValues()
|
||||
{
|
||||
var source = new StructBaseProperty { MasterPropertyId = 0xA0u };
|
||||
var array = new ArrayBaseProperty { MasterPropertyId = 0xA1u };
|
||||
array.Value.Add(new IntegerBaseProperty { MasterPropertyId = 0xA2u, Value = -17 });
|
||||
array.Value.Add(new Bitfield64BaseProperty
|
||||
{
|
||||
MasterPropertyId = 0xA3u,
|
||||
Value = 0xFEDCBA9876543210ul,
|
||||
});
|
||||
source.Value[0x55u] = array;
|
||||
source.Value[0x56u] = new VectorBaseProperty
|
||||
{
|
||||
MasterPropertyId = 0xA4u,
|
||||
Value = new Vector3(1.25f, -2.5f, 9.75f),
|
||||
};
|
||||
|
||||
var converted = LayoutImporter.ConvertProperty(source);
|
||||
|
||||
Assert.Equal(UiPropertyKind.Struct, converted.Kind);
|
||||
Assert.Equal(0xA0u, converted.MasterPropertyId);
|
||||
var convertedArray = converted.StructValue[0x55u];
|
||||
Assert.Equal(UiPropertyKind.Array, convertedArray.Kind);
|
||||
Assert.Equal(0xA1u, convertedArray.MasterPropertyId);
|
||||
Assert.Equal(-17, convertedArray.ArrayValue[0].IntegerValue);
|
||||
Assert.Equal(0xFEDCBA9876543210ul, convertedArray.ArrayValue[1].UnsignedValue);
|
||||
Assert.Equal(new Vector3(1.25f, -2.5f, 9.75f), converted.StructValue[0x56u].VectorValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertProperty_PreservesStringInfoAndColorBytes()
|
||||
{
|
||||
var text = new StringInfoBaseProperty
|
||||
{
|
||||
Value = new StringInfo
|
||||
{
|
||||
Token = 7,
|
||||
StringId = 0x12345678u,
|
||||
TableId = 0x23000001u,
|
||||
Override = StringInfoOverrideFlag.AutoGen,
|
||||
English = 2,
|
||||
Comment = 3,
|
||||
},
|
||||
};
|
||||
var color = new ColorBaseProperty
|
||||
{
|
||||
Value = new ColorARGB { Blue = 1, Green = 2, Red = 3, Alpha = 4 },
|
||||
};
|
||||
|
||||
var convertedText = LayoutImporter.ConvertProperty(text);
|
||||
var convertedColor = LayoutImporter.ConvertProperty(color);
|
||||
|
||||
Assert.Equal(
|
||||
new UiStringInfoValue(7, 0x12345678u, 0x23000001u, 2, 2, 3),
|
||||
convertedText.StringInfoValue);
|
||||
Assert.Equal(new UiColorValue(1, 2, 3, 4), convertedColor.ColorValue);
|
||||
}
|
||||
|
||||
private static UiPropertyValue Bool(bool value)
|
||||
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
||||
|
||||
private static UiPropertyValue Enum(uint value)
|
||||
=> new() { Kind = UiPropertyKind.Enum, UnsignedValue = value };
|
||||
|
||||
private static UiStateInfo State(
|
||||
uint id,
|
||||
string name,
|
||||
params (uint Id, UiPropertyValue Value)[] properties)
|
||||
{
|
||||
var state = new UiStateInfo { Id = id, Name = name };
|
||||
foreach (var property in properties)
|
||||
state.Properties.Values[property.Id] = property.Value;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,10 @@ public class VitalsBindingTests
|
|||
public void Bind_SetsHealthMeterFillFromProvider()
|
||||
{
|
||||
var health = new UiMeter();
|
||||
var layout = FakeLayout((VitalsController.Health, health));
|
||||
var healthText = new UiText();
|
||||
var layout = FakeLayout(
|
||||
(VitalsController.Health, health),
|
||||
(VitalsController.HealthText, healthText));
|
||||
float hp = 0.42f;
|
||||
|
||||
VitalsController.Bind(layout,
|
||||
|
|
@ -28,9 +31,9 @@ public class VitalsBindingTests
|
|||
manaText: () => "");
|
||||
|
||||
Assert.Equal(0.42f, health.Fill()!.Value);
|
||||
// The meter no longer draws its own label; the cur/max is a centered UiText child.
|
||||
// The meter no longer draws its own label; the authored text node is bound in place.
|
||||
Assert.Null(health.Label());
|
||||
Assert.Equal("42/100", NumberText(health));
|
||||
Assert.Equal("42/100", NumberText(healthText));
|
||||
}
|
||||
|
||||
// ── Test 2: All three meters wired to distinct providers ──────────────────
|
||||
|
|
@ -41,10 +44,16 @@ public class VitalsBindingTests
|
|||
var health = new UiMeter();
|
||||
var stamina = new UiMeter();
|
||||
var mana = new UiMeter();
|
||||
var healthText = new UiText();
|
||||
var staminaText = new UiText();
|
||||
var manaText = new UiText();
|
||||
var layout = FakeLayout(
|
||||
(VitalsController.Health, health),
|
||||
(VitalsController.Stamina, stamina),
|
||||
(VitalsController.Mana, mana));
|
||||
(VitalsController.Mana, mana),
|
||||
(VitalsController.HealthText, healthText),
|
||||
(VitalsController.StaminaText, staminaText),
|
||||
(VitalsController.ManaText, manaText));
|
||||
|
||||
VitalsController.Bind(layout,
|
||||
healthPct: () => 0.25f,
|
||||
|
|
@ -56,13 +65,13 @@ public class VitalsBindingTests
|
|||
|
||||
// Each meter should reflect its own provider, not another's.
|
||||
Assert.Equal(0.25f, health.Fill()!.Value);
|
||||
Assert.Equal("25/100", NumberText(health));
|
||||
Assert.Equal("25/100", NumberText(healthText));
|
||||
|
||||
Assert.Equal(0.50f, stamina.Fill()!.Value);
|
||||
Assert.Equal("50/100", NumberText(stamina));
|
||||
Assert.Equal("50/100", NumberText(staminaText));
|
||||
|
||||
Assert.Equal(0.75f, mana.Fill()!.Value);
|
||||
Assert.Equal("75/100", NumberText(mana));
|
||||
Assert.Equal("75/100", NumberText(manaText));
|
||||
}
|
||||
|
||||
// ── Test 3: Missing meter ids are silently skipped (no throw) ─────────────
|
||||
|
|
@ -89,12 +98,12 @@ public class VitalsBindingTests
|
|||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The cur/max text from the centered <see cref="UiText"/> number that
|
||||
/// <see cref="VitalsController"/> attaches as the meter's child.</summary>
|
||||
private static string NumberText(UiMeter m)
|
||||
/// <summary>The cur/max text from the authored <see cref="UiText"/> node.</summary>
|
||||
private static string NumberText(UiText num)
|
||||
{
|
||||
var num = Assert.IsType<UiText>(m.Children[0]);
|
||||
Assert.True(num.Centered);
|
||||
Assert.True(num.OneLine);
|
||||
Assert.False(num.Selectable);
|
||||
var lines = num.LinesProvider();
|
||||
return lines.Count > 0 ? lines[0].Text : "";
|
||||
}
|
||||
|
|
|
|||
30486
tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json
Normal file
30486
tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
17057
tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json
Normal file
17057
tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json
Normal file
File diff suppressed because it is too large
Load diff
11218
tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json
Normal file
11218
tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
12227
tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json
Normal file
12227
tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue