diff --git a/README.md b/README.md index 9909ad1..70d0c4a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,11 @@ Streaming is off until you turn it on. The shared secret is never compiled in: `/mm help` lists every command. +The plugin has one button on the plugin bar, MM. Its Metas tab downloads the +club's metas and navs into the shared VTank profile folder the client +configures (by default `%LOCALAPPDATA%\OpenAC\vtank`), under `mosstank\metas` +and `mosstank\navs`, where MossTank loads them; the tab shows both folders. + ## Testing against a stand-in backend `tools/FrameRecorder` accepts the plugin's socket like the backend does, diff --git a/src/OpenAC.MosswartMassacre/FeatureCatalog.cs b/src/OpenAC.MosswartMassacre/FeatureCatalog.cs index 5ace75f..de2ebc8 100644 --- a/src/OpenAC.MosswartMassacre/FeatureCatalog.cs +++ b/src/OpenAC.MosswartMassacre/FeatureCatalog.cs @@ -79,14 +79,7 @@ internal static class FeatureCatalog context.Host.Ui.ShowPanel("flagtracker"); }; } - if (metas.Panel is { } metasPanel) - { - mainWindow.MetasOpener = () => - { - metasPanel.IsVisible = true; - context.Host.Ui.ShowPanel("metas"); - }; - } + mainWindow.MetasTab = metas.Panel; // Inventory: full_inventory, inventory_delta, equipment_cantrip_state, // and the taper count telemetry reads (InventoryFeature.PrismaticTaperCount). diff --git a/src/OpenAC.MosswartMassacre/FlagTracker/FlagTrackerFeature.cs b/src/OpenAC.MosswartMassacre/FlagTracker/FlagTrackerFeature.cs index 4087620..6879694 100644 --- a/src/OpenAC.MosswartMassacre/FlagTracker/FlagTrackerFeature.cs +++ b/src/OpenAC.MosswartMassacre/FlagTracker/FlagTrackerFeature.cs @@ -4,7 +4,8 @@ using OpenAC.MosswartMassacre.Quests; namespace OpenAC.MosswartMassacre.FlagTracker; /// -/// The "Mossy Tracker" window: augmentations, luminance auras, recalls, +/// The "Mossy Tracker" window, opened from the main window's Mossy Tracker +/// tab: augmentations, luminance auras, recalls, /// cantrips, tracked weapons and quest timers for the character in the world. /// Nothing here reaches the backend. /// @@ -47,13 +48,12 @@ internal sealed class FlagTrackerFeature : IMmFeature _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, + // No bar button: as in the original, the main window's + // Mossy Tracker tab opens it. + ShowInSidePanel = false, }, Path.Combine(directory, MarkupFile), Panel); diff --git a/src/OpenAC.MosswartMassacre/Metas/MetaSyncFeature.cs b/src/OpenAC.MosswartMassacre/Metas/MetaSyncFeature.cs index 0fd528c..4031770 100644 --- a/src/OpenAC.MosswartMassacre/Metas/MetaSyncFeature.cs +++ b/src/OpenAC.MosswartMassacre/Metas/MetaSyncFeature.cs @@ -6,7 +6,8 @@ namespace OpenAC.MosswartMassacre.Metas; /// /// Meta and nav sync from the club's repository: the /mm metasrefresh, /// metascheck, metaspull and metaspullfile verbs and, on a -/// host with a window, the Metas window. Nothing here reaches the backend. +/// host with a window, the state behind the main window's Metas tab. Nothing +/// here reaches the backend. /// /// /// Network work runs on the thread pool; every result, chat line and window @@ -15,15 +16,11 @@ namespace OpenAC.MosswartMassacre.Metas; /// internal sealed class MetaSyncFeature : IMmFeature { - /// The panel's markup file, beside the plugin assembly. - public const string MarkupFile = "mosswartmassacre-metas.xml"; - private readonly MmContext _context; private readonly MetaSyncManager _manager; private readonly ConcurrentQueue _onTick = new(); private readonly CancellationTokenSource _shutdown = new(); private readonly Action _tick; - private readonly IDisposable? _panel; private Task _lastOperation = Task.CompletedTask; private bool _disposed; @@ -55,28 +52,17 @@ internal sealed class MetaSyncFeature : IMmFeature return; Panel = new MetasViewModel(); - Panel.ShowPath(_manager.StorageRoot); + ShowFolders(); + Panel.RepositoryText = RepositoryUrl; Panel.ShowFiles([]); Panel.ButtonPressed += OnButton; Panel.SearchRequested += ShowFiles; - - string directory = Path.GetDirectoryName(typeof(MetaSyncFeature).Assembly.Location) ?? "."; - _panel = context.Host.Ui.RegisterPanel( - new PluginPanelDescriptor("metas", "Metas") - { - IconText = "ME", - // 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); + Panel.RepositoryChanged += OnRepositoryChanged; } /// - /// The window's binding object; set its IsVisible to open the - /// window. Null on a host without a window. + /// The Metas tab's state, which the main window binds to. Null on a host + /// without a window. /// public MetasViewModel? Panel { get; } @@ -95,7 +81,14 @@ internal sealed class MetaSyncFeature : IMmFeature } } - public void OnLogin() => Panel?.ShowPath(_manager.StorageRoot); + public void OnLogin() + { + if (Panel is null) + return; + // The repository is a per-character setting, loaded at login. + ShowFolders(); + Panel.RepositoryText = RepositoryUrl; + } public void OnLogoff() { @@ -112,8 +105,8 @@ internal sealed class MetaSyncFeature : IMmFeature { Panel.ButtonPressed -= OnButton; Panel.SearchRequested -= ShowFiles; + Panel.RepositoryChanged -= OnRepositoryChanged; } - _panel?.Dispose(); _onTick.Clear(); // Operations run one after another, so the manager goes once the // last one, cancelled above, has finished. @@ -239,7 +232,7 @@ internal sealed class MetaSyncFeature : IMmFeature switch (button) { case nameof(MetasViewModel.RefreshList): - panel.ShowPath(_manager.StorageRoot); + ShowFolders(); panel.StatusText = "Status: refreshing remote file list..."; RunRefresh( count => panel.StatusText = $"Status: loaded {count} remote files", @@ -251,7 +244,7 @@ internal sealed class MetaSyncFeature : IMmFeature break; case nameof(MetasViewModel.CheckUpdates): - panel.ShowPath(_manager.StorageRoot); + ShowFolders(); panel.StatusText = "Status: checking remote updates..."; RunCheck( summary => panel.StatusText = @@ -299,6 +292,32 @@ internal sealed class MetaSyncFeature : IMmFeature } } + private void ShowFolders() => + Panel?.ShowFolders( + _manager.FolderPath(MetaSyncManager.MetaFolder), + _manager.FolderPath(MetaSyncManager.NavFolder)); + + /// + /// Stores the address the player entered, or the default for an empty + /// one. The list already shown came from the old address, so the status + /// asks for a refresh. + /// + private void OnRepositoryChanged(string address) + { + if (Panel is null) + return; + string trimmed = address.Trim(); + if (trimmed.Length > 0 && !Uri.TryCreate(trimmed, UriKind.Absolute, out _)) + { + Panel.StatusText = "Status: not a repository address"; + return; + } + _context.Settings.Current.MetasRepositoryUrl = trimmed; + _context.Settings.Save(); + Panel.RepositoryText = RepositoryUrl; + Panel.StatusText = "Status: repository set; press Refresh List"; + } + private void ShowFiles() { if (Panel is null) diff --git a/src/OpenAC.MosswartMassacre/Metas/MetaSyncManager.cs b/src/OpenAC.MosswartMassacre/Metas/MetaSyncManager.cs index c84a798..b72fa7a 100644 --- a/src/OpenAC.MosswartMassacre/Metas/MetaSyncManager.cs +++ b/src/OpenAC.MosswartMassacre/Metas/MetaSyncManager.cs @@ -105,9 +105,25 @@ internal sealed class MetaSyncManager : IDisposable public DateTime LastStatusCheckUtc { get; private set; } = DateTime.MinValue; - /// The storage root the files are written beneath, for the path label; null without storage. + /// The storage root the files are written beneath; null without storage. public string? StorageRoot => _storage.IsAvailable ? _storage.RootPath ?? string.Empty : null; + /// + /// The folder on disk a storage folder ( or + /// ) sits in, for telling the player where the + /// files went: the storage key itself when the storage has no folder on + /// disk, null without storage. + /// + public string? FolderPath(string folder) + { + string? root = StorageRoot; + if (root is null) + return null; + return root.Length == 0 + ? folder + : Path.Combine(root, folder.Replace('/', Path.DirectorySeparatorChar)); + } + public IReadOnlyList GetCachedFiles() { lock (_sync) diff --git a/src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs b/src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs index a72beea..a38c417 100644 --- a/src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs +++ b/src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs @@ -3,10 +3,11 @@ using System.Globalization; namespace OpenAC.MosswartMassacre.Metas; /// -/// The binding object of the Metas window (mosswartmassacre-metas.xml), -/// the original's Metas tab: the repository's files with their status, a -/// live search, a pick list for one file, and the refresh, check and -/// download buttons. Assigned on the tick thread only. +/// The state behind the main window's Metas tab: the repository's files +/// with their status, a live search, a pick list for one file, the refresh, +/// check and download buttons, the repository address and the folders the +/// files land in. The main window forwards its Metas bindings here. +/// Assigned on the tick thread only. /// internal sealed class MetasViewModel { @@ -30,6 +31,7 @@ internal sealed class MetasViewModel CheckUpdates = () => ButtonPressed?.Invoke(nameof(CheckUpdates)); DownloadOutdated = () => ButtonPressed?.Invoke(nameof(DownloadOutdated)); DownloadSelected = () => ButtonPressed?.Invoke(nameof(DownloadSelected)); + SetRepository = address => RepositoryChanged?.Invoke(address ?? string.Empty); } /// A button was pressed; the argument is its binding name. @@ -38,13 +40,17 @@ internal sealed class MetasViewModel /// The search text changed or was cleared. internal event Action? SearchRequested; - /// - /// Whether the window is open. The root panel's visibility is bound to it, - /// so the main window opens this one by setting it; it starts closed. - /// - public bool IsVisible { get; set; } + /// The player entered a repository address; empty means the default. + internal event Action? RepositoryChanged; - public string PathText { get; set; } = "VTank Path: unresolved"; + public string MetaFolderText { get; set; } = UnresolvedMetaFolder; + + public string NavFolderText { get; set; } = UnresolvedNavFolder; + + /// The repository address the files come from. + public string RepositoryText { get; set; } = string.Empty; + + public Action SetRepository { get; } public string LastCheckText { get; set; } = "Last Check: never"; @@ -147,8 +153,16 @@ internal sealed class MetasViewModel $"Last Check: {DateTime.SpecifyKind(stamp, DateTimeKind.Utc).ToLocalTime():yyyy-MM-dd HH:mm:ss}"); } - public void ShowPath(string? storageRoot) => - PathText = storageRoot is null ? "VTank Path: unresolved" : $"VTank Path: {storageRoot}"; + /// Names the folders the metas and navs land in; null when there is no profile storage. + public void ShowFolders(string? metaFolder, string? navFolder) + { + MetaFolderText = metaFolder is null ? UnresolvedMetaFolder : $"Metas: {metaFolder}"; + NavFolderText = navFolder is null ? UnresolvedNavFolder : $"Navs: {navFolder}"; + } + + private const string UnresolvedMetaFolder = "Metas: no profile folder"; + + private const string UnresolvedNavFolder = "Navs: no profile folder"; /// The status text colour: green current, orange outdated, blue missing, red failed. public static uint StatusColor(MetaFileSyncStatus status) => status switch diff --git a/src/OpenAC.MosswartMassacre/Views/MainWindow.Metas.cs b/src/OpenAC.MosswartMassacre/Views/MainWindow.Metas.cs new file mode 100644 index 0000000..327ba28 --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Views/MainWindow.Metas.cs @@ -0,0 +1,59 @@ +using OpenAC.MosswartMassacre.Metas; + +namespace OpenAC.MosswartMassacre.Views; + +/// +/// The Metas tab's bindings. The markup binds one object per window, so each +/// is forwarded to the meta sync's ; an action +/// looks the tab up when it runs, so it works whenever the tab is set. +/// +internal sealed partial class MainWindow +{ + private static readonly IReadOnlyList NoItems = []; + + private static readonly IReadOnlyList NoColors = []; + + public string MetasFolderText => MetasTab?.MetaFolderText ?? string.Empty; + + public string NavsFolderText => MetasTab?.NavFolderText ?? string.Empty; + + public string MetasRepositoryText => MetasTab?.RepositoryText ?? string.Empty; + + public Action SetMetasRepository => address => MetasTab?.SetRepository(address); + + public Action MetasRefreshList => () => MetasTab?.RefreshList(); + + public Action MetasCheckUpdates => () => MetasTab?.CheckUpdates(); + + public Action MetasDownloadOutdated => () => MetasTab?.DownloadOutdated(); + + public Action MetasDownloadSelected => () => MetasTab?.DownloadSelected(); + + public string MetasSearchText => MetasTab?.SearchText ?? string.Empty; + + public Action MetasSearchChanged => text => MetasTab?.SearchChanged(text); + + public Action MetasClearSearch => () => MetasTab?.ClearSearch(); + + public int MetasNoRow => -1; + + public Action MetasIgnoreRow => _ => { }; + + public IReadOnlyList MetasFileTypes => MetasTab?.FileTypes ?? NoItems; + + public IReadOnlyList MetasFilePaths => MetasTab?.FilePaths ?? NoItems; + + public IReadOnlyList MetasFileStatus => MetasTab?.FileStatus ?? NoItems; + + public IReadOnlyList MetasFileStatusColors => MetasTab?.FileStatusColors ?? NoColors; + + public IReadOnlyList MetasSelectionItems => MetasTab?.SelectionItems ?? NoItems; + + public string MetasSelectedLabel => MetasTab?.SelectedLabel ?? string.Empty; + + public Action MetasSelectionChanged => label => MetasTab?.SelectionChanged(label); + + public string MetasLastCheckText => MetasTab?.LastCheckText ?? string.Empty; + + public string MetasStatusText => MetasTab?.StatusText ?? string.Empty; +} diff --git a/src/OpenAC.MosswartMassacre/Views/MainWindow.cs b/src/OpenAC.MosswartMassacre/Views/MainWindow.cs index ee7a080..06b3275 100644 --- a/src/OpenAC.MosswartMassacre/Views/MainWindow.cs +++ b/src/OpenAC.MosswartMassacre/Views/MainWindow.cs @@ -1,18 +1,23 @@ using System.Globalization; using AcDream.Plugin.Abstractions; +using OpenAC.MosswartMassacre.Metas; using OpenAC.MosswartMassacre.Streams; namespace OpenAC.MosswartMassacre.Views; /// -/// The main window (graphical host only) and its binding object. Every value -/// is read live from the trackers each frame, so nothing here needs to be told -/// that a number changed. +/// The main window (graphical host only) and its binding object: the one +/// plugin-bar button, "MM" with the original window's icon, and the +/// original's tabbed window. Every value is read live each frame, so nothing +/// here needs to be told that a number changed. /// -internal sealed class MainWindow : IMmFeature +internal sealed partial class MainWindow : IMmFeature { public const string MarkupFile = "mosswartmassacre-main.xml"; + /// The original window's icon, 7735 in the image table. + public const uint OriginalIcon = 0x06001E37; + private readonly MmContext _context; private readonly SessionStats _stats; private IDisposable? _registration; @@ -30,6 +35,8 @@ internal sealed class MainWindow : IMmFeature new PluginPanelDescriptor("main", "Mosswart Massacre") { IconText = "MM", + // The icon the original window carried (7735). + IconSurfaceId = OriginalIcon, StartVisible = false, ShowInSidePanel = true, }, @@ -42,24 +49,30 @@ internal sealed class MainWindow : IMmFeature Main, Settings, Statistics, + MossyTracker, + Metas, Tools, } /// Opens the flag tracker window; set by the feature that owns it. public Action? FlagTrackerOpener { get; set; } - /// Opens the metas window; set by the feature that owns it. - public Action? MetasOpener { get; set; } + /// The Metas tab's state; set by the feature that owns it. Null on a host without a window. + public MetasViewModel? MetasTab { get; set; } // ── Tabs ── public bool MainSelected => _page == Page.Main; public bool SettingsSelected => _page == Page.Settings; public bool StatisticsSelected => _page == Page.Statistics; + public bool MossyTrackerSelected => _page == Page.MossyTracker; + public bool MetasSelected => _page == Page.Metas; public bool ToolsSelected => _page == Page.Tools; public Action ShowMain => () => _page = Page.Main; public Action ShowSettings => () => _page = Page.Settings; public Action ShowStatistics => () => _page = Page.Statistics; + public Action ShowMossyTracker => () => _page = Page.MossyTracker; + public Action ShowMetas => () => _page = Page.Metas; public Action ShowTools => () => _page = Page.Tools; // ── Main ── @@ -173,7 +186,7 @@ internal sealed class MainWindow : IMmFeature public Action ResetStats => () => _context.Commands.Dispatch("reset"); - // ── Tools ── + // ── Mossy Tracker ── public Action OpenFlagTracker => () => { @@ -183,13 +196,7 @@ internal sealed class MainWindow : IMmFeature _context.Chat.Write("The Mossy Tracker is not available."); }; - public Action OpenMetas => () => - { - if (MetasOpener is { } open) - open(); - else - _context.Chat.Write("The metas window is not available."); - }; + // ── Tools ── public Action SendInventory => () => _context.Commands.Dispatch("sendinventory"); diff --git a/src/OpenAC.MosswartMassacre/mosswartmassacre-main.xml b/src/OpenAC.MosswartMassacre/mosswartmassacre-main.xml index de4fcf0..d28ad0c 100644 --- a/src/OpenAC.MosswartMassacre/mosswartmassacre-main.xml +++ b/src/OpenAC.MosswartMassacre/mosswartmassacre-main.xml @@ -1,21 +1,28 @@ - - + + - - - - + + - + - + - +