Flag tracker: the Mossy Tracker window as a markup panel

The owner calls the flag tracker important, so the original's pages come
across with their tables unchanged: augmentations and luminance auras from
the player's own int properties (the object table carries them), recalls
from the spellbook with composited spell icons, cantrips matched from the
active enchantments to trained skills, attributes and wards, the tracked
weapons the original kept data for but never showed, and the quest timers.
Read on the tick thread at login and on each page's Refresh; the quest
page redraws every five seconds instead of from a thread-pool timer.

The root panel's visibility is the settable IsVisible so the main window
can open it; the three sections the original left as stubs stay out, and a
headless host registers nothing. The fake spell catalog learns IsKnown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-25 12:20:52 +02:00
parent 0b5da797e0
commit 714fbfa2e8
8 changed files with 1735 additions and 0 deletions

View file

@ -0,0 +1,239 @@
using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.FlagTracker;
using OpenAC.MosswartMassacre.Tests.Fakes;
namespace OpenAC.MosswartMassacre.Tests.FlagTracker;
public sealed class FlagTrackerTests
{
private const uint Player = 0x50000001;
private static PluginHarness WithUi(Action<FakeHost>? arrange = null) =>
new(arrange: host =>
{
host.HasUi = true;
arrange?.Invoke(host);
});
private static void SetPlayerInts(FakeHost host, Dictionary<uint, int> ints) =>
host.Automation.Objects.Properties[Player] = new PluginItemProperties(
ints,
new Dictionary<uint, long>(),
new Dictionary<uint, bool>(),
new Dictionary<uint, double>(),
new Dictionary<uint, string>(),
new Dictionary<uint, uint>(),
new Dictionary<uint, uint>());
private static FlagTrackerViewModel Panel(PluginHarness harness) =>
harness.Feature<FlagTrackerFeature>().Panel!;
private static int Row(IReadOnlyList<string> names, string name)
{
int index = names.ToList().IndexOf(name);
Assert.True(index >= 0, $"no row {name}");
return index;
}
[Fact]
public void A_headless_host_gets_no_window_and_login_does_nothing()
{
using var harness = new PluginHarness();
harness.Login();
harness.Run(6);
FlagTrackerFeature feature = harness.Feature<FlagTrackerFeature>();
Assert.Null(feature.Panel);
Assert.Empty(harness.Host.Ui.Panels);
}
[Fact]
public void A_graphical_host_gets_the_mossy_tracker_window_closed()
{
using var harness = WithUi();
RegisteredPanel panel = Assert.Single(harness.Host.Ui.Panels);
Assert.Equal("flagtracker", panel.Descriptor.WindowId);
Assert.Equal("Mossy Tracker", panel.Descriptor.Title);
Assert.True(panel.Descriptor.ShowInSidePanel);
Assert.EndsWith(FlagTrackerFeature.MarkupFile, panel.MarkupPath, StringComparison.Ordinal);
Assert.Same(Panel(harness), panel.Binding);
Assert.False(Panel(harness).IsVisible);
Assert.True(Panel(harness).AugsSelected);
Panel(harness).ShowQuests();
Assert.True(Panel(harness).QuestsSelected);
Assert.False(Panel(harness).AugsSelected);
}
[Fact]
public void Augmentations_come_from_the_players_own_properties()
{
using var harness = WithUi(host => SetPlayerInts(host, new() { [231] = 3, [230] = 2 }));
harness.Login();
FlagTrackerViewModel panel = Panel(harness);
Assert.Equal("--- Death Augs ---", panel.AugNames[0]);
int keepItems = Row(panel.AugNames, "Keep Items");
Assert.Equal("[MAX]", panel.AugProgress[keepItems]);
Assert.Equal(0x008000u, panel.AugProgressColors[keepItems]);
Assert.Equal("Rohula bint Ludun", panel.AugTrainers[keepItems]);
Assert.Equal("Ayan Baqur", panel.AugLocations[keepItems]);
int keepSpells = Row(panel.AugNames, "Keep Spells");
Assert.Equal("[0/1]", panel.AugProgress[keepSpells]);
Assert.Equal(0xFF0000u, panel.AugProgressColors[keepSpells]);
int carrying = Row(panel.AugNames, "Extra Carrying Capacity");
Assert.Equal("[2/5]", panel.AugProgress[carrying]);
Assert.Equal(0xFFFF00u, panel.AugProgressColors[carrying]);
// A bought augmentation shows after Refresh.
SetPlayerInts(harness.Host, new() { [232] = 1 });
panel.RefreshAugs();
Assert.Equal("[MAX]", panel.AugProgress[keepSpells]);
}
[Fact]
public void Luminance_auras_show_progress_and_the_seer_they_belong_to()
{
using var harness = WithUi(host => SetPlayerInts(host, new() { [333] = 5, [344] = 2 }));
harness.Login();
FlagTrackerViewModel panel = Panel(harness);
int damage = Row(panel.LumNames, "+1 Damage Rating");
Assert.Equal("5/5", panel.LumProgress[damage]);
Assert.Equal(0x008000u, panel.LumProgressColors[damage]);
Assert.Equal("Nalicana Auras", panel.LumCategories[damage]);
int kahiri = Row(panel.LumNames, "(Ka'hiri) +2 Specialized Skills");
Assert.Equal("2/5", panel.LumProgress[kahiri]);
Assert.Equal(0xFFFF00u, panel.LumProgressColors[kahiri]);
Assert.Equal("LoyalToKahiri", panel.LumCategories[kahiri]);
}
[Fact]
public void Recalls_show_known_and_unknown_with_their_spell_icons()
{
using var harness = WithUi(host => host.Automation.Spells.KnownSpellIds.Add(2023));
harness.Login();
FlagTrackerViewModel panel = Panel(harness);
Assert.Equal(17, panel.RecallNames.Count);
Assert.Equal("Recall the Sanctuary", panel.RecallNames[0]);
Assert.Equal(2023u, panel.RecallSpellIds[0]);
Assert.Equal("Known", panel.RecallStatus[0]);
Assert.Equal(0x008000u, panel.RecallStatusColors[0]);
Assert.Equal("Unknown", panel.RecallStatus[1]);
Assert.Equal(0xFF0000u, panel.RecallStatusColors[1]);
}
[Fact]
public void Cantrips_are_matched_to_trained_skills_attributes_and_wards()
{
using var harness = WithUi(host =>
{
host.Automation.Character.Skills.Add(new PluginSkillInfo(34, "War Magic", PluginSkillTraining.Specialized, 400));
host.Automation.Character.Skills.Add(new PluginSkillInfo(6, "Melee Defense", PluginSkillTraining.Trained, 300));
host.Automation.Character.Skills.Add(new PluginSkillInfo(21, "Healing", PluginSkillTraining.Untrained, 10));
host.Automation.Spells.All =
[
FakeSpellCatalog.Spell(4715, "Epic War Magic Aptitude"),
FakeSpellCatalog.Spell(2248, "Major Invulnerability"),
FakeSpellCatalog.Spell(1, "Minor Invulnerability"),
FakeSpellCatalog.Spell(6099, "Legendary Flame Ward"),
FakeSpellCatalog.Spell(2091, "Major Strength"),
FakeSpellCatalog.Spell(2, "Strength Self VIII"),
];
host.Automation.Character.ActiveEnchantments =
[
new PluginActiveEnchantment(4715, 0, 0, -1),
new PluginActiveEnchantment(2248, 0, 0, -1),
new PluginActiveEnchantment(1, 0, 0, -1),
new PluginActiveEnchantment(6099, 0, 0, -1),
new PluginActiveEnchantment(2091, 0, 0, -1),
new PluginActiveEnchantment(2, 0, 0, 3600),
];
});
harness.Login();
FlagTrackerViewModel panel = Panel(harness);
Assert.Equal(
[
"--- Specialized Skills ---", "War Magic",
"--- Trained Skills ---", "Invulnerability",
"--- Attributes ---", "Strength",
"--- Protection Auras ---", "Armor", "Bludgeoning Ward", "Piercing Ward",
"Slashing Ward", "Flame Ward", "Frost Ward", "Acid Ward", "Storm Ward",
],
panel.CantripNames);
Assert.Equal(FlagTrackerData.Icons.CategoryStar, panel.CantripIcons[0]);
Assert.Equal("Epic", panel.CantripLevels[1]);
Assert.Equal(0x800080u, panel.CantripLevelColors[1]);
Assert.Equal(FlagTrackerData.Icons.GreenCircle, panel.CantripIcons[1]);
// The higher of two cantrips on one skill wins.
Assert.Equal("Major", panel.CantripLevels[3]);
Assert.Equal(0x0000FFu, panel.CantripLevelColors[3]);
Assert.Equal("Major", panel.CantripLevels[5]);
Assert.Equal("N/A", panel.CantripLevels[7]);
Assert.Equal(FlagTrackerData.Icons.RedCircle, panel.CantripIcons[7]);
int flame = Row(panel.CantripNames, "Flame Ward");
Assert.Equal("Legendary", panel.CantripLevels[flame]);
Assert.Equal(0xFFA500u, panel.CantripLevelColors[flame]);
}
[Fact]
public void Tracked_weapons_are_found_in_the_inventory_by_name()
{
using var harness = WithUi(host => host.Automation.Items.Owned.Add(
FakeItems.Item(0x80000001, "Atlan Sword") with { IconId = 0x06001234 }));
harness.Login();
FlagTrackerViewModel panel = Panel(harness);
int atlan = Row(panel.WeaponNames, "Atlan Sword");
Assert.Equal("Acquired", panel.WeaponStatus[atlan]);
Assert.Equal(0x06001234u, panel.WeaponIcons[atlan]);
int axe = Row(panel.WeaponNames, "Atlan Axe");
Assert.Equal("Not Acquired", panel.WeaponStatus[axe]);
Assert.Equal(FlagTrackerData.Icons.Portal, panel.WeaponIcons[axe]);
}
[Fact]
public void The_quest_page_hides_maxed_quests_and_sorts_by_solves()
{
using var harness = WithUi();
harness.Login();
long now = new DateTimeOffset(harness.Clock.UtcNow).ToUnixTimeSeconds();
var chat = harness.Host.Automation.Chat;
chat.Deliver($"stipendtimer_0812 - 2 solves ({now - 10})\"x\" -1 3700");
chat.Deliver($"maxed - 5 solves ({now})\"x\" 5 60");
chat.Deliver($"aaa - 7 solves ({now - 100})\"x\" -1 50");
FlagTrackerViewModel panel = Panel(harness);
Assert.Equal(["aaa", "Stipend Collection Timer"], panel.QuestRowNames);
Assert.Equal(["READY", "1h 01m"], panel.QuestCountdowns);
Assert.Equal([0x008000u, 0xFFFF00u], panel.QuestCountdownColors);
Assert.Equal(["50s", "1h 1m 40s"], panel.QuestCooldowns);
Assert.Equal(["7", "2"], panel.QuestSolves);
// The countdowns move on every five seconds.
harness.Run(5.1);
Assert.Equal("1h 01m", panel.QuestCountdowns[1]);
harness.Run(120);
Assert.StartsWith("59m ", panel.QuestCountdowns[1], StringComparison.Ordinal);
}
[Fact]
public void Logoff_empties_the_pages()
{
using var harness = WithUi();
harness.Login();
Assert.NotEmpty(Panel(harness).AugNames);
harness.Plugin.OnLogoff();
Assert.Empty(Panel(harness).AugNames);
Assert.Empty(Panel(harness).QuestRowNames);
}
}