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,159 @@
using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Quests;
namespace OpenAC.MosswartMassacre.FlagTracker;
/// <summary>
/// The "Mossy Tracker" window: augmentations, luminance auras, recalls,
/// cantrips, tracked weapons and quest timers for the character in the world.
/// Nothing here reaches the backend.
/// </summary>
/// <remarks>
/// The window exists only on a host that draws one; on a headless host this
/// feature registers nothing and does nothing. The pages are read at login
/// and whenever the player presses a page's Refresh button, as the original
/// read them when its window opened and on Refresh; the quest page follows
/// the quest list and redraws its countdowns every five seconds.
/// </remarks>
internal sealed class FlagTrackerFeature : IMmFeature
{
/// <summary>The panel's markup file, beside the plugin assembly.</summary>
public const string MarkupFile = "mosswartmassacre-flagtracker.xml";
/// <summary>How often the quest countdowns are redrawn.</summary>
public static readonly TimeSpan QuestRedrawInterval = TimeSpan.FromSeconds(5);
private readonly MmContext _context;
private readonly QuestManager _quests;
private readonly FlagTrackerData _data;
private readonly IDisposable? _panel;
private readonly Action? _onQuestsChanged;
private IDisposable? _questRedraw;
public FlagTrackerFeature(MmContext context, QuestManager quests)
{
_context = context;
_quests = quests;
_data = new FlagTrackerData(context.Automation);
if (!context.Host.HasUi)
return;
Panel = new FlagTrackerViewModel();
Panel.RefreshRequested += OnRefreshRequested;
_onQuestsChanged = ShowQuests;
_quests.QuestsChanged += _onQuestsChanged;
string directory = Path.GetDirectoryName(typeof(FlagTrackerFeature).Assembly.Location) ?? ".";
_panel = context.Host.Ui.RegisterPanel(
new PluginPanelDescriptor("flagtracker", "Mossy Tracker")
{
IconText = "MT",
// The icon the original window carried (7735).
IconSurfaceId = 0x06001E37,
// Shown while the binding's IsVisible is set; the host's own
// shown state starts true so that flag alone opens it.
StartVisible = true,
ShowInSidePanel = true,
},
Path.Combine(directory, MarkupFile),
Panel);
}
/// <summary>
/// The window's binding object; set its <c>IsVisible</c> to open the
/// window. Null on a host without a window.
/// </summary>
public FlagTrackerViewModel? Panel { get; }
internal FlagTrackerData Data => _data;
public void OnLogin()
{
if (Panel is null)
return;
RefreshAll();
_questRedraw?.Dispose();
_questRedraw = _context.Scheduler.Every(QuestRedrawInterval, ShowQuests);
}
public void OnLogoff()
{
_questRedraw?.Dispose();
_questRedraw = null;
Panel?.Clear();
}
public void Dispose()
{
OnLogoff();
if (_onQuestsChanged is not null)
_quests.QuestsChanged -= _onQuestsChanged;
if (Panel is not null)
Panel.RefreshRequested -= OnRefreshRequested;
_panel?.Dispose();
}
/// <summary>Reads every page again and redraws it.</summary>
public void RefreshAll()
{
if (Panel is null)
return;
try
{
_data.RefreshAll();
}
catch (Exception ex)
{
_context.Chat.Write($"[MossyTracker] Data refresh failed: {ex.Message}");
}
Panel.ShowData(_data);
ShowQuests();
}
private void ShowQuests()
{
if (Panel is null || !_context.IsLoggedIn)
return;
Panel.ShowQuestRows(_quests.QuestList, _quests.UnixNow);
}
private void OnRefreshRequested(string button)
{
if (Panel is null || !_context.IsLoggedIn)
return;
try
{
switch (button)
{
case nameof(FlagTrackerViewModel.RefreshAugs):
_data.RefreshAugmentations();
Panel.ShowAugmentations(_data);
break;
case nameof(FlagTrackerViewModel.RefreshLum):
_data.RefreshLuminanceAuras();
Panel.ShowLuminance(_data);
break;
case nameof(FlagTrackerViewModel.RefreshRecalls):
_data.RefreshRecallSpells();
Panel.ShowRecallSpells(_data);
break;
case nameof(FlagTrackerViewModel.RefreshCantrips):
_data.RefreshCantrips();
Panel.ShowCantripRows(_data);
break;
case nameof(FlagTrackerViewModel.RefreshWeapons):
_data.RefreshWeapons();
Panel.ShowWeaponRows(_data);
break;
case nameof(FlagTrackerViewModel.RefreshQuests):
_quests.RefreshQuests();
ShowQuests();
break;
}
}
catch (Exception ex)
{
_context.Chat.Write($"[MossyTracker] Refresh failed: {ex.Message}");
}
}
}