diff --git a/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj b/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj index 957c01406..1bb753011 100644 --- a/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj +++ b/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj @@ -11,6 +11,14 @@ + + diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index 11f67c764..1da42204f 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -471,6 +471,14 @@ internal sealed partial class MossTankPanel public IReadOnlyList AdvancedOptionCategoryNames { get; } = VtankOptionCatalog.CategoryBits.Select(bit => $"0x{bit:X}").ToArray(); public IReadOnlyList AdvancedOptionCategoryEnabled => _advancedOptionCategoryEnabled; + // Fix round B item 15's own build-over-real-files test caught this: + // markup REQUIRES a "selected" int binding (MarkupDocument. + // BindRequiredIntReader throws "must be an int binding" when the + // attribute is missing) even for a pure checklist with no real + // selection concept — item 10 already removed the row-selection band + // rendering for every column grid, so this value drives no visible + // highlight; it exists purely to satisfy the markup grammar. + public int SelectedAdvancedOptionCategoryIndex => -1; public Action ToggleAdvancedOptionCategoryAt => index => { if ((uint)index >= (uint)_advancedOptionCategoryEnabled.Length) diff --git a/src/AcDream.Plugins.MossTank/mosstank-advanced.xml b/src/AcDream.Plugins.MossTank/mosstank-advanced.xml index 91dab2938..a99cf30fd 100644 --- a/src/AcDream.Plugins.MossTank/mosstank-advanced.xml +++ b/src/AcDream.Plugins.MossTank/mosstank-advanced.xml @@ -81,7 +81,12 @@ + diff --git a/tests/AcDream.App.Tests/AcDream.App.Tests.csproj b/tests/AcDream.App.Tests/AcDream.App.Tests.csproj index b8dc39980..9caeae5d2 100644 --- a/tests/AcDream.App.Tests/AcDream.App.Tests.csproj +++ b/tests/AcDream.App.Tests/AcDream.App.Tests.csproj @@ -24,6 +24,15 @@ + + @@ -52,6 +61,11 @@ PreserveNewest + + diff --git a/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs b/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs new file mode 100644 index 000000000..ca2faac54 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs @@ -0,0 +1,99 @@ +using AcDream.App.UI; +using AcDream.Plugin.Abstractions; +using AcDream.Plugins.MossTank; +using Xunit; + +namespace AcDream.App.Tests.UI; + +/// +/// Fix round B item 15. Every existing MossTank markup pin +/// (MossTankMarkupContractTests) validates mosstank*.xml +/// against through reflection alone — "does a +/// public property with this name and this CLR type exist" — never through +/// itself, the code that actually mounts +/// a plugin panel at runtime. MarkupDocument.Build has its own +/// validation a reflection-only check can't see (attribute-format +/// exceptions like ValidateArtStyle's "must be plain or retail", +/// delegate-shape mismatches surfaced as thrown s +/// rather than a missing property, numeric-attribute parsing, column-type +/// dispatch). Before this test, a markup bug of that shape would throw +/// inside RetailUiRuntime.MountPlugins's own try/catch and the panel +/// would simply not appear — no test failure, no visible error short of a +/// live client screenshot. This builds every real mosstank*.xml file +/// against a REAL (a stub , +/// same shape as MossTankMarkupContractTests.StubHost) so a bad +/// attribute fails a test instead of dropping the panel silently at mount. +/// +public sealed class MossTankMarkupBuildOverRealFilesTests +{ + private static string MossTankMarkupDirectory => + Path.Combine(AppContext.BaseDirectory, "MossTank"); + + public static IEnumerable MossTankMarkupFiles() => + Directory.GetFiles(MossTankMarkupDirectory, "mosstank*.xml") + .OrderBy(static path => path, StringComparer.Ordinal) + .Select(static path => new object[] { path }); + + [Theory] + [MemberData(nameof(MossTankMarkupFiles))] + public void EveryMossTankPanelFileBuildsAgainstARealPanelWithNoException(string path) + { + string xml = File.ReadAllText(path); + var panel = new MossTankPanel(new StubHost()); + + UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); + + Assert.NotNull(built); + Assert.NotEmpty(built.Children); + } + + private sealed class StubHost : IPluginHost + { + public bool HasUi => false; + public IPluginLogger Log { get; } = new StubLogger(); + public IGameState State { get; } = new StubState(); + public IEvents Events { get; } = new StubEvents(); + public ISelectionService Selection { get; } = new StubSelection(); + public IUiRegistry Ui => NoOpUiRegistry.Instance; + public IAutomationSurface Automation => NoOpAutomationSurface.Instance; + } + + private sealed class StubLogger : IPluginLogger + { + public void Info(string message) { } + public void Warn(string message) { } + public void Error(string message, Exception? exception = null) { } + } + + private sealed class StubState : IGameState + { + public IReadOnlyList Entities => []; + } + + private sealed class StubEvents : IEvents + { + public event Action EntitySpawned + { + add { } + remove { } + } + public event Action Tick + { + add { } + remove { } + } + } + + private sealed class StubSelection : ISelectionService + { + public uint? SelectedObjectId => null; + public uint? PreviousObjectId => null; + public event Action Changed + { + add { } + remove { } + } + public bool Select(uint objectId) => false; + public bool Clear() => false; + } +} diff --git a/tests/AcDream.App.Tests/packages.neutral.lock.json b/tests/AcDream.App.Tests/packages.neutral.lock.json index 352f60401..2d1df398d 100644 --- a/tests/AcDream.App.Tests/packages.neutral.lock.json +++ b/tests/AcDream.App.Tests/packages.neutral.lock.json @@ -448,6 +448,12 @@ "acdream.plugin.abstractions": { "type": "Project" }, + "acdream.plugins.mosstank": { + "type": "Project", + "dependencies": { + "AcDream.Plugin.Abstractions": "[1.0.0, )" + } + }, "acdream.runtime": { "type": "Project", "dependencies": {