feat(ui): share indicator detail panels
Port the authored Link Status, Vitae, and Mini Game detail roots and register every indicator page with retail's one-active gmPanelUI owner. Helpful/Harmful and the new pages now replace Inventory, Character, or Magic at one canonical window position while preserving the DAT restore-previous flag. Correct the retail ping wire to its payload-free request/response, publish measured RTT, and port Vitae recovery XP from the live modifier and player properties. Keep transport packet-loss averaging and mini-game gameplay explicitly tracked under AP-110. Release build and all 5,814 tests pass with five intentional skips. Connected visual gate pending. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
52c529be86
commit
a96767ba6d
28 changed files with 6539 additions and 32 deletions
|
|
@ -143,6 +143,24 @@ public static class FixtureLoader
|
|||
public static ElementInfo LoadIndicatorsInfos()
|
||||
=> LoadInfos("indicators_21000071.json");
|
||||
|
||||
public static ImportedLayout LoadLinkStatus()
|
||||
=> LayoutImporter.Build(LoadLinkStatusInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadLinkStatusInfos()
|
||||
=> LoadInfos("link_status_2100001D.json");
|
||||
|
||||
public static ImportedLayout LoadVitae()
|
||||
=> LayoutImporter.Build(LoadVitaeInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadVitaeInfos()
|
||||
=> LoadInfos("vitae_21000020.json");
|
||||
|
||||
public static ImportedLayout LoadMiniGame()
|
||||
=> LayoutImporter.Build(LoadMiniGameInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadMiniGameInfos()
|
||||
=> LoadInfos("mini_game_2100001E.json");
|
||||
|
||||
// ── Shared loader ────────────────────────────────────────────────────────
|
||||
|
||||
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ public sealed class IndicatorBarControllerTests
|
|||
50u, 3u, 60f, 1u, StatModValue: 0.99f, Bucket: 4u));
|
||||
Assert.True(vitae.Enabled);
|
||||
Assert.Equal(UiButtonStateMachine.Normal, vitae.ActiveRetailStateId);
|
||||
vitae.OnEvent(new UiEvent(0, vitae, UiEventType.Click));
|
||||
Assert.Equal(RetailPanelCatalog.Vitae, h.ToggledPanels[^1]);
|
||||
|
||||
h.Spellbook.OnEnchantmentRemoved(3u, 50u);
|
||||
Assert.False(vitae.Enabled);
|
||||
|
|
@ -164,6 +166,8 @@ public sealed class IndicatorBarControllerTests
|
|||
Assert.False(miniGame.Enabled);
|
||||
controller.SetMiniGameActive(true);
|
||||
Assert.True(miniGame.Enabled);
|
||||
miniGame.OnEvent(new UiEvent(0, miniGame, UiEventType.Click));
|
||||
Assert.Equal(RetailPanelCatalog.MiniGame, h.ToggledPanels[^1]);
|
||||
controller.SetMiniGameActive(false);
|
||||
Assert.False(miniGame.Enabled);
|
||||
|
||||
|
|
@ -171,6 +175,18 @@ public sealed class IndicatorBarControllerTests
|
|||
Assert.Equal(1, h.EndSessionRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkButton_DispatchesSharedLinkStatusPanel()
|
||||
{
|
||||
var h = CreateHarness();
|
||||
using IndicatorBarController controller = h.Controller;
|
||||
UiButton link = h.Button(IndicatorBarController.LinkButtonId);
|
||||
|
||||
link.OnEvent(new UiEvent(0, link, UiEventType.Click));
|
||||
|
||||
Assert.Equal([RetailPanelCatalog.LinkStatus], h.ToggledPanels);
|
||||
}
|
||||
|
||||
private static Harness CreateHarness(int strength = 10)
|
||||
{
|
||||
var table = SpellTable.LoadFromReader(new StringReader(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class IndicatorDetailPanelControllerTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void AuthoredFixtures_AreRetailMainPanelPages()
|
||||
{
|
||||
AssertPage(
|
||||
FixtureLoader.LoadLinkStatusInfos(),
|
||||
LinkStatusUiController.RootId,
|
||||
0x1000001Du,
|
||||
LinkStatusUiController.MainTextId,
|
||||
LinkStatusUiController.CloseId);
|
||||
AssertPage(
|
||||
FixtureLoader.LoadVitaeInfos(),
|
||||
VitaeUiController.RootId,
|
||||
0x10000020u,
|
||||
VitaeUiController.MainTextId,
|
||||
VitaeUiController.CloseId);
|
||||
AssertPage(
|
||||
FixtureLoader.LoadMiniGameInfos(),
|
||||
MiniGameUiController.RootId,
|
||||
0x1000001Eu,
|
||||
0x1000016Eu,
|
||||
MiniGameUiController.CloseId);
|
||||
|
||||
Assert.Equal(8u, RetailPanelCatalog.LinkStatus);
|
||||
Assert.Equal(9u, RetailPanelCatalog.MiniGame);
|
||||
Assert.Equal(15u, RetailPanelCatalog.Vitae);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkStatus_RequestsPingOnShow_RefreshesText_AndRepeatsAfter120Seconds()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadLinkStatus();
|
||||
double now = 0d;
|
||||
int pings = 0;
|
||||
int closes = 0;
|
||||
LinkStatusSnapshot snapshot = new(
|
||||
Connected: true,
|
||||
SecondsSinceLastPacket: 0.25d,
|
||||
PacketLossPercentage: 1.25d,
|
||||
RoundTripSeconds: 0.123d);
|
||||
using LinkStatusUiController controller = LinkStatusUiController.Bind(
|
||||
layout,
|
||||
() => snapshot,
|
||||
() => now,
|
||||
() => pings++,
|
||||
LinkStatusStrings.English,
|
||||
() => closes++)!;
|
||||
|
||||
controller.OnShown();
|
||||
|
||||
Assert.Equal(1, pings);
|
||||
string text = VisibleText(layout, LinkStatusUiController.MainTextId);
|
||||
Assert.Contains("Link Indicator", text, StringComparison.Ordinal);
|
||||
Assert.Contains("1.25", text, StringComparison.Ordinal);
|
||||
Assert.Contains("123", text, StringComparison.Ordinal);
|
||||
|
||||
controller.Tick();
|
||||
now = 119d;
|
||||
controller.Tick();
|
||||
Assert.Equal(1, pings);
|
||||
now = 124d;
|
||||
controller.Tick();
|
||||
Assert.Equal(2, pings);
|
||||
|
||||
UiButton close = Assert.IsType<UiButton>(layout.FindElement(LinkStatusUiController.CloseId));
|
||||
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
||||
Assert.Equal(1, closes);
|
||||
|
||||
controller.OnHidden();
|
||||
now = 300d;
|
||||
controller.Tick();
|
||||
Assert.Equal(2, pings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vitae_UsesDeathLevelPoolAndRetailThresholdFormula()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadVitae();
|
||||
var spellbook = new Spellbook();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Player });
|
||||
objects.UpdateIntProperty(Player, VitaeUiController.DeathLevelProperty, 10);
|
||||
objects.UpdateIntProperty(Player, VitaeUiController.VitaeCpPoolProperty, 100);
|
||||
using VitaeUiController controller = VitaeUiController.Bind(
|
||||
layout,
|
||||
spellbook,
|
||||
objects,
|
||||
() => Player,
|
||||
VitaeStrings.English)!;
|
||||
|
||||
Assert.Contains(
|
||||
"full strength",
|
||||
VisibleText(layout, VitaeUiController.MainTextId),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||
SpellId: 666u,
|
||||
LayerId: 1u,
|
||||
Duration: -1f,
|
||||
CasterGuid: Player,
|
||||
StatModValue: 0.9f,
|
||||
Bucket: 4u));
|
||||
|
||||
string text = VisibleText(layout, VitaeUiController.MainTextId);
|
||||
Assert.Contains("lost 10 %", text, StringComparison.Ordinal);
|
||||
Assert.Contains("earn 379 more experience", text, StringComparison.Ordinal);
|
||||
Assert.Equal(479, VitaeUiController.VitaeCpPoolThreshold(0.9f, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MiniGameFixture_BindsItsAuthoredCloseButton()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadMiniGame();
|
||||
int closes = 0;
|
||||
using MiniGameUiController controller = MiniGameUiController.Bind(
|
||||
layout, () => closes++)!;
|
||||
|
||||
UiButton close = Assert.IsType<UiButton>(layout.FindElement(MiniGameUiController.CloseId));
|
||||
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
||||
|
||||
Assert.Equal(1, closes);
|
||||
}
|
||||
|
||||
private static void AssertPage(
|
||||
ElementInfo info,
|
||||
uint rootId,
|
||||
uint type,
|
||||
uint contentId,
|
||||
uint closeId)
|
||||
{
|
||||
Assert.Equal(rootId, info.Id);
|
||||
Assert.Equal(type, info.Type);
|
||||
Assert.Equal((300f, 362f), (info.Width, info.Height));
|
||||
Assert.NotNull(Find(info, contentId));
|
||||
Assert.NotNull(Find(info, closeId));
|
||||
Assert.True(info.TryGetEffectiveBool(
|
||||
RetailPanelUiController.RestorePreviousPropertyId,
|
||||
out bool restorePrevious));
|
||||
Assert.True(restorePrevious);
|
||||
}
|
||||
|
||||
private static string VisibleText(ImportedLayout layout, uint id)
|
||||
{
|
||||
UiText text = Assert.IsType<UiText>(layout.FindElement(id));
|
||||
return string.Join(' ', text.LinesProvider().Select(line => line.Text));
|
||||
}
|
||||
|
||||
private static ElementInfo? Find(ElementInfo root, uint id)
|
||||
{
|
||||
if (root.Id == id) return root;
|
||||
foreach (ElementInfo child in root.Children)
|
||||
if (Find(child, id) is { } found) return found;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
{
|
||||
(0x21000006u, "chat_21000006.json"),
|
||||
(0x21000016u, "toolbar_21000016.json"),
|
||||
(0x2100001Du, "link_status_2100001D.json"),
|
||||
(0x21000020u, "vitae_21000020.json"),
|
||||
(0x21000023u, "inventory_21000023.json"),
|
||||
(0x21000024u, "paperdoll_21000024.json"),
|
||||
(0x2100002Eu, "character_2100002E.json"),
|
||||
|
|
@ -118,6 +120,17 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), effectsJson);
|
||||
}
|
||||
|
||||
ElementInfo? miniGame = LayoutImporter.ImportInfos(
|
||||
dats, MiniGameUiController.LayoutId, MiniGameUiController.RootId);
|
||||
Assert.NotNull(miniGame);
|
||||
File.WriteAllText(
|
||||
Path.Combine(FixtureDirectory(), "mini_game_2100001E.json"),
|
||||
JsonSerializer.Serialize(miniGame, new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true,
|
||||
WriteIndented = true,
|
||||
}));
|
||||
|
||||
foreach ((uint rootId, string fileName) in new[]
|
||||
{
|
||||
(0x10000466u,
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ public sealed class RetailPanelUiControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RestorePreviousEffect_DoesNotReplaceMainPanelPlacement()
|
||||
public void IndicatorDetailPanel_SharesPlacementAndRestoresPreviousPanel()
|
||||
{
|
||||
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||
RetailWindowHandle inventory = Mount(root, WindowNames.Inventory, 40f, 60f);
|
||||
|
|
@ -122,13 +122,17 @@ public sealed class RetailPanelUiControllerTests
|
|||
using var controller = Create(root);
|
||||
controller.RegisterMainPanel(7u, WindowNames.Inventory, inventory);
|
||||
controller.RegisterMainPanel(11u, WindowNames.Character, character);
|
||||
controller.Register(4u, WindowNames.PositiveEffects, restorePrevious: true);
|
||||
controller.RegisterMainPanel(
|
||||
4u,
|
||||
WindowNames.PositiveEffects,
|
||||
effect,
|
||||
restorePrevious: true);
|
||||
|
||||
controller.SetPanelVisibility(7u, visible: true);
|
||||
inventory.MoveTo(280f, 120f);
|
||||
controller.SetPanelVisibility(4u, visible: true);
|
||||
|
||||
Assert.Equal((900f, 30f), (effect.Left, effect.Top));
|
||||
Assert.Equal((280f, 120f), (effect.Left, effect.Top));
|
||||
Assert.False(inventory.IsVisible);
|
||||
Assert.True(effect.IsVisible);
|
||||
|
||||
|
|
|
|||
1059
tests/AcDream.App.Tests/UI/Layout/fixtures/link_status_2100001D.json
Normal file
1059
tests/AcDream.App.Tests/UI/Layout/fixtures/link_status_2100001D.json
Normal file
File diff suppressed because it is too large
Load diff
3402
tests/AcDream.App.Tests/UI/Layout/fixtures/mini_game_2100001E.json
Normal file
3402
tests/AcDream.App.Tests/UI/Layout/fixtures/mini_game_2100001E.json
Normal file
File diff suppressed because it is too large
Load diff
1059
tests/AcDream.App.Tests/UI/Layout/fixtures/vitae_21000020.json
Normal file
1059
tests/AcDream.App.Tests/UI/Layout/fixtures/vitae_21000020.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue