diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9a095ed
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,127 @@
+# AGENTS.md
+
+Guidance for coding agents working in `MosswartMassacre` (DECAL plugin).
+
+See shared cross-repo guidance first: `../AGENTS.md`.
+
+## Scope and architecture
+
+- This repo is a C# plugin for Asheron's Call using DECAL + VirindiViewService.
+- Main projects in solution `mossy.sln`:
+- `MosswartMassacre/MosswartMassacre.csproj` (primary plugin)
+- `MosswartMassacre.Loader/MosswartMassacre.Loader.csproj` (loader)
+- Target framework is `.NET Framework 4.8` (`net48`), x86 runtime expectations.
+- The plugin emits WebSocket events consumed by `MosswartOverlord`.
+
+## Rule sources discovered
+
+- Cursor rules: none found (`.cursor/rules/` missing, `.cursorrules` missing).
+- Copilot rules: none found (`.github/copilot-instructions.md` missing).
+- Follow cross-repo protocol rules in `../AGENTS.md` for payload compatibility.
+
+## Build commands
+
+## Solution build
+
+- Build Debug solution (recommended during development):
+- `msbuild "mossy.sln" /p:Configuration=Debug /p:Platform="Any CPU"`
+- Build Release solution:
+- `msbuild "mossy.sln" /p:Configuration=Release /p:Platform="Any CPU"`
+
+## Project build
+
+- Build primary plugin project:
+- `msbuild "MosswartMassacre/MosswartMassacre.csproj" /p:Configuration=Debug /p:Platform="AnyCPU"`
+- Build loader project:
+- `msbuild "MosswartMassacre.Loader/MosswartMassacre.Loader.csproj" /p:Configuration=Debug /p:Platform="AnyCPU"`
+
+## Restore dependencies
+
+- Legacy packages restore (when needed):
+- `nuget restore "mossy.sln"`
+- If package restore issues appear, verify `packages/` and `packages.config` state.
+
+## Lint/format/test status
+
+- No repository-wide C# linter/formatter config was found (`.editorconfig`, StyleCop, dotnet-format config not present).
+- No automated unit/integration test project was found in this repo.
+- Validation is primarily build + in-game runtime verification.
+
+## Single-test guidance (practical)
+
+- Because no automated test suite exists, "single test" means targeted manual verification.
+- Preferred single-scenario checks:
+- Toggle one plugin command and validate behavior (example: `/mm telemetry on`).
+- Trigger one event path (example: spawn or chat) and verify backend receives it.
+- For backend-coupled checks, also tail backend logs in `MosswartOverlord`.
+
+## Runtime/integration quick checks
+
+- Verify plugin loads in DECAL and opens the VVS tabbed UI.
+- Confirm settings persist per character via YAML file.
+- Confirm WebSocket registration and periodic telemetry emission.
+- Confirm one inventory/full-inventory or delta message reaches backend.
+- Confirm no repeated event handler subscriptions after hot reload.
+
+## Important coupling points
+
+- WebSocket endpoint currently defined in `MosswartMassacre/WebSocket.cs`.
+- Shared secret header logic also in `MosswartMassacre/WebSocket.cs`.
+- Event envelope fields and `type` values must stay backend-compatible.
+- Avoid unilateral changes to JSON keys (`character_name`, coords, timestamps, etc.).
+
+## Code style conventions observed
+
+## Language and formatting
+
+- Use C# 8-compatible syntax; do not rely on newer language-only features.
+- Use 4-space indentation and braces on new lines (existing style).
+- Keep files UTF-8 and preserve existing BOM behavior where present.
+- Keep methods focused; prefer extracting helpers over deeply nested blocks.
+
+## Imports/usings
+
+- Group `using` statements at top of file.
+- Order with `System*` namespaces first, then third-party, then project namespaces.
+- Remove unused `using` directives when touching a file.
+
+## Naming and structure
+
+- Public types/methods/properties: `PascalCase`.
+- Local variables/private fields: `camelCase`; private static fields commonly `_prefixed`.
+- Constants: `UPPER_SNAKE_CASE` or `PascalCase` according to existing file style.
+- Keep event handler names descriptive (`OnX`, `HandleX`, `..._LoginComplete`).
+
+## Error handling and logging
+
+- Guard plugin startup/shutdown with robust exception handling.
+- For external boundaries (WebSocket, file I/O, DECAL hooks), catch and log failures.
+- Prefer non-crashing failure behavior with clear in-chat or logger diagnostics.
+- Preserve existing logging patterns (`IPluginLogger`, plugin chat output) in touched files.
+
+## Concurrency and event safety
+
+- Be careful with timers, event subscriptions, and hot-reload lifecycle.
+- Always unsubscribe handlers during cleanup to prevent duplicate callbacks.
+- Avoid blocking calls on game/UI event threads.
+- Keep thread-affinity concerns explicit when interacting with UI/game APIs.
+
+## Settings and persistence
+
+- Settings are YAML-backed and character-specific (`PluginSettings`).
+- Preserve atomic save behavior (temp file + replace/move pattern).
+- Add new settings as optional with safe defaults to keep backward compatibility.
+
+## Payload and API conventions
+
+- Emit JSON with stable, snake_case field names expected by backend.
+- Include ISO8601 timestamps and parseable coordinate values.
+- Keep envelope `type` values stable unless backend changes in same task.
+- Prefer additive changes (new optional fields) over renames/removals.
+
+## Repo hygiene for agents
+
+- Keep edits minimal and task-scoped.
+- Do not mass-reformat unrelated files.
+- Document behavior changes in README or relevant docs when needed.
+- If introducing new build/test tooling, add command docs to this file.
diff --git a/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj b/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
index 59c8e03..3e3dbc2 100644
--- a/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
+++ b/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
@@ -30,8 +30,8 @@
False
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL
+ ..\MosswartMassacre\lib\Decal.Interop.Core.DLL
False
-
\ No newline at end of file
+
diff --git a/MosswartMassacre/MetaSyncManager.cs b/MosswartMassacre/MetaSyncManager.cs
new file mode 100644
index 0000000..d52e20d
--- /dev/null
+++ b/MosswartMassacre/MetaSyncManager.cs
@@ -0,0 +1,419 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using Microsoft.Win32;
+using Newtonsoft.Json.Linq;
+
+namespace MosswartMassacre
+{
+ internal enum MetaFileSyncStatus
+ {
+ Unknown,
+ NotDownloaded,
+ UpToDate,
+ UpdateAvailable,
+ Error
+ }
+
+ internal sealed class MetaFileEntry
+ {
+ public string RelativePath { get; set; }
+ public string FileName { get; set; }
+ public string Extension { get; set; }
+ public string RemoteBlobSha { get; set; }
+ public string LocalPath { get; set; }
+ public bool LocalExists { get; set; }
+ public string LocalSha256 { get; set; }
+ public string RemoteSha256 { get; set; }
+ public MetaFileSyncStatus Status { get; set; }
+ public string StatusText { get; set; }
+ }
+
+ internal sealed class MetaDownloadResult
+ {
+ public string RelativePath { get; set; }
+ public bool Downloaded { get; set; }
+ public bool SkippedAsCurrent { get; set; }
+ public bool BackupCreated { get; set; }
+ public string Message { get; set; }
+ }
+
+ internal static class MetaSyncManager
+ {
+ private const string RepoOwner = "SawatoMosswartsEnjoyersClub";
+ private const string RepoName = "metas";
+ private const string BranchName = "main";
+ private const string ApiBaseUrl = "https://git.snakedesert.se/api/v1";
+ private const string RawBaseUrl = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/metas/raw/branch/main";
+
+ private static readonly HttpClient HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
+ private static readonly object Sync = new object();
+ private static List _cachedFiles = new List();
+
+ public static DateTime LastCatalogRefreshUtc { get; private set; } = DateTime.MinValue;
+ public static DateTime LastStatusCheckUtc { get; private set; } = DateTime.MinValue;
+
+ public static IReadOnlyList GetCachedFiles()
+ {
+ lock (Sync)
+ {
+ return _cachedFiles
+ .Select(CloneEntry)
+ .ToList()
+ .AsReadOnly();
+ }
+ }
+
+ public static string GetResolvedVtankPath()
+ {
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(PluginSettings.Instance?.VTankProfilesPath) &&
+ Directory.Exists(PluginSettings.Instance.VTankProfilesPath))
+ {
+ return PluginSettings.Instance.VTankProfilesPath;
+ }
+
+ var defaultPath = @"C:\Games\VirindiPlugins\VirindiTank\";
+
+ try
+ {
+ var regKey = Registry.LocalMachine.OpenSubKey("Software\\Decal\\Plugins\\{642F1F48-16BE-48BF-B1D4-286652C4533E}");
+ if (regKey != null)
+ {
+ var profilePath = regKey.GetValue("ProfilePath")?.ToString();
+ if (!string.IsNullOrWhiteSpace(profilePath) && Directory.Exists(profilePath))
+ {
+ return profilePath;
+ }
+ }
+ }
+ catch
+ {
+ }
+
+ return defaultPath;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ public static async Task RefreshCatalogAsync()
+ {
+ var remoteEntries = await FetchRemoteTreeEntriesAsync();
+ var vtankPath = GetResolvedVtankPath();
+
+ var result = remoteEntries
+ .Where(path => path.EndsWith(".met", StringComparison.OrdinalIgnoreCase) ||
+ path.EndsWith(".nav", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
+ .Select(path =>
+ {
+ var localPath = string.IsNullOrWhiteSpace(vtankPath)
+ ? string.Empty
+ : Path.Combine(vtankPath, Path.GetFileName(path));
+ var exists = !string.IsNullOrWhiteSpace(localPath) && File.Exists(localPath);
+ return new MetaFileEntry
+ {
+ RelativePath = path,
+ FileName = Path.GetFileName(path),
+ Extension = Path.GetExtension(path)?.TrimStart('.').ToLowerInvariant() ?? string.Empty,
+ RemoteBlobSha = string.Empty,
+ LocalPath = localPath,
+ LocalExists = exists,
+ Status = exists ? MetaFileSyncStatus.Unknown : MetaFileSyncStatus.NotDownloaded,
+ StatusText = exists ? "Unknown" : "Not downloaded"
+ };
+ })
+ .ToList();
+
+ lock (Sync)
+ {
+ _cachedFiles = result;
+ LastCatalogRefreshUtc = DateTime.UtcNow;
+ }
+
+ return result.Count;
+ }
+
+ public static async Task<(int upToDate, int needsUpdate, int notDownloaded, int errors)> CheckStatusesAsync()
+ {
+ List workingSet;
+ lock (Sync)
+ {
+ workingSet = _cachedFiles.Select(CloneEntry).ToList();
+ }
+
+ if (workingSet.Count == 0)
+ {
+ await RefreshCatalogAsync();
+ lock (Sync)
+ {
+ workingSet = _cachedFiles.Select(CloneEntry).ToList();
+ }
+ }
+
+ foreach (var entry in workingSet)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(entry.LocalPath) || !File.Exists(entry.LocalPath))
+ {
+ entry.LocalExists = false;
+ entry.LocalSha256 = string.Empty;
+ entry.Status = MetaFileSyncStatus.NotDownloaded;
+ entry.StatusText = "Not downloaded";
+ continue;
+ }
+
+ entry.LocalExists = true;
+ entry.LocalSha256 = CalculateFileSha256(entry.LocalPath);
+
+ var remoteBytes = await DownloadRemoteFileBytesAsync(entry.RelativePath);
+ entry.RemoteSha256 = CalculateSha256(remoteBytes);
+
+ if (string.Equals(entry.LocalSha256, entry.RemoteSha256, StringComparison.OrdinalIgnoreCase))
+ {
+ entry.Status = MetaFileSyncStatus.UpToDate;
+ entry.StatusText = "Up to date";
+ }
+ else
+ {
+ entry.Status = MetaFileSyncStatus.UpdateAvailable;
+ entry.StatusText = "Update available";
+ }
+ }
+ catch (Exception ex)
+ {
+ entry.Status = MetaFileSyncStatus.Error;
+ entry.StatusText = "Error";
+ PluginCore.WriteToChat($"[Metas] Status check failed for {entry.RelativePath}: {ex.Message}");
+ }
+ }
+
+ lock (Sync)
+ {
+ _cachedFiles = workingSet;
+ LastStatusCheckUtc = DateTime.UtcNow;
+ }
+
+ var upToDate = workingSet.Count(x => x.Status == MetaFileSyncStatus.UpToDate);
+ var needsUpdate = workingSet.Count(x => x.Status == MetaFileSyncStatus.UpdateAvailable);
+ var notDownloaded = workingSet.Count(x => x.Status == MetaFileSyncStatus.NotDownloaded);
+ var errors = workingSet.Count(x => x.Status == MetaFileSyncStatus.Error);
+
+ return (upToDate, needsUpdate, notDownloaded, errors);
+ }
+
+ public static async Task DownloadFileAsync(string relativePath)
+ {
+ var result = new MetaDownloadResult
+ {
+ RelativePath = relativePath,
+ Downloaded = false,
+ SkippedAsCurrent = false,
+ BackupCreated = false,
+ Message = string.Empty
+ };
+
+ if (string.IsNullOrWhiteSpace(relativePath))
+ {
+ result.Message = "No file selected.";
+ return result;
+ }
+
+ var vtankPath = GetResolvedVtankPath();
+ if (string.IsNullOrWhiteSpace(vtankPath))
+ {
+ result.Message = "VTank path could not be resolved.";
+ return result;
+ }
+
+ if (!Directory.Exists(vtankPath))
+ {
+ Directory.CreateDirectory(vtankPath);
+ }
+
+ var localPath = Path.Combine(vtankPath, Path.GetFileName(relativePath));
+ var remoteBytes = await DownloadRemoteFileBytesAsync(relativePath);
+ var remoteHash = CalculateSha256(remoteBytes);
+
+ if (File.Exists(localPath))
+ {
+ var localHash = CalculateFileSha256(localPath);
+ if (string.Equals(localHash, remoteHash, StringComparison.OrdinalIgnoreCase))
+ {
+ result.SkippedAsCurrent = true;
+ result.Message = "Already up to date.";
+ await UpdateSingleCachedEntryAsync(relativePath, localPath, localHash, remoteHash, MetaFileSyncStatus.UpToDate, "Up to date");
+ return result;
+ }
+
+ var backupPath = localPath + ".bak";
+ File.Copy(localPath, backupPath, true);
+ result.BackupCreated = true;
+ }
+
+ var tempPath = localPath + ".tmp";
+ File.WriteAllBytes(tempPath, remoteBytes);
+
+ if (File.Exists(localPath))
+ {
+ File.Delete(localPath);
+ }
+
+ File.Move(tempPath, localPath);
+
+ result.Downloaded = true;
+ result.Message = result.BackupCreated ? "Downloaded and updated (.bak created)." : "Downloaded.";
+
+ await UpdateSingleCachedEntryAsync(relativePath, localPath, CalculateFileSha256(localPath), remoteHash, MetaFileSyncStatus.UpToDate, "Up to date");
+
+ return result;
+ }
+
+ public static async Task<(int downloaded, int skipped, int failed, int backups)> DownloadOutdatedAsync()
+ {
+ List current;
+ lock (Sync)
+ {
+ current = _cachedFiles.Select(CloneEntry).ToList();
+ }
+
+ if (current.Count == 0)
+ {
+ await RefreshCatalogAsync();
+ current = GetCachedFiles().Select(CloneEntry).ToList();
+ }
+
+ var candidates = current
+ .Where(x => x.Status == MetaFileSyncStatus.UpdateAvailable || x.Status == MetaFileSyncStatus.NotDownloaded || x.Status == MetaFileSyncStatus.Unknown)
+ .OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ var downloaded = 0;
+ var skipped = 0;
+ var failed = 0;
+ var backups = 0;
+
+ foreach (var entry in candidates)
+ {
+ try
+ {
+ var result = await DownloadFileAsync(entry.RelativePath);
+ if (result.Downloaded) downloaded++;
+ if (result.SkippedAsCurrent) skipped++;
+ if (result.BackupCreated) backups++;
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ PluginCore.WriteToChat($"[Metas] Download failed for {entry.RelativePath}: {ex.Message}");
+ await UpdateSingleCachedEntryAsync(entry.RelativePath, entry.LocalPath, entry.LocalSha256, entry.RemoteSha256, MetaFileSyncStatus.Error, "Error");
+ }
+ }
+
+ return (downloaded, skipped, failed, backups);
+ }
+
+ private static async Task> FetchRemoteTreeEntriesAsync()
+ {
+ var treeUrl = $"{ApiBaseUrl}/repos/{RepoOwner}/{RepoName}/git/trees/{BranchName}?recursive=1";
+ using (var response = await HttpClient.GetAsync(treeUrl))
+ {
+ response.EnsureSuccessStatusCode();
+ var json = await response.Content.ReadAsStringAsync();
+ var parsed = JObject.Parse(json);
+ var tree = parsed["tree"] as JArray;
+ if (tree == null)
+ {
+ return new List();
+ }
+
+ return tree
+ .Where(x => string.Equals(x["type"]?.ToString(), "blob", StringComparison.OrdinalIgnoreCase))
+ .Select(x => x["path"]?.ToString())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .ToList();
+ }
+ }
+
+ private static async Task DownloadRemoteFileBytesAsync(string relativePath)
+ {
+ var escapedPath = string.Join("/", (relativePath ?? string.Empty)
+ .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(Uri.EscapeDataString));
+ var fileUrl = $"{RawBaseUrl}/{escapedPath}";
+ return await HttpClient.GetByteArrayAsync(fileUrl);
+ }
+
+ private static string CalculateFileSha256(string path)
+ {
+ using (var stream = File.OpenRead(path))
+ {
+ using (var sha = SHA256.Create())
+ {
+ return ToLowerHex(sha.ComputeHash(stream));
+ }
+ }
+ }
+
+ private static string CalculateSha256(byte[] data)
+ {
+ using (var sha = SHA256.Create())
+ {
+ return ToLowerHex(sha.ComputeHash(data));
+ }
+ }
+
+ private static string ToLowerHex(byte[] hash)
+ {
+ return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
+ }
+
+ private static async Task UpdateSingleCachedEntryAsync(string relativePath, string localPath, string localHash, string remoteHash, MetaFileSyncStatus status, string statusText)
+ {
+ await Task.Run(() =>
+ {
+ lock (Sync)
+ {
+ var target = _cachedFiles.FirstOrDefault(x => string.Equals(x.RelativePath, relativePath, StringComparison.OrdinalIgnoreCase));
+ if (target == null)
+ {
+ return;
+ }
+
+ target.LocalPath = localPath;
+ target.LocalExists = !string.IsNullOrWhiteSpace(localPath) && File.Exists(localPath);
+ target.LocalSha256 = localHash ?? string.Empty;
+ target.RemoteSha256 = remoteHash ?? string.Empty;
+ target.Status = status;
+ target.StatusText = statusText ?? "Unknown";
+ }
+ });
+ }
+
+ private static MetaFileEntry CloneEntry(MetaFileEntry entry)
+ {
+ return new MetaFileEntry
+ {
+ RelativePath = entry.RelativePath,
+ FileName = entry.FileName,
+ Extension = entry.Extension,
+ RemoteBlobSha = entry.RemoteBlobSha,
+ LocalPath = entry.LocalPath,
+ LocalExists = entry.LocalExists,
+ LocalSha256 = entry.LocalSha256,
+ RemoteSha256 = entry.RemoteSha256,
+ Status = entry.Status,
+ StatusText = entry.StatusText
+ };
+ }
+ }
+}
diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 56d93d1..35c2a9a 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -321,6 +321,7 @@
+
@@ -365,16 +366,6 @@
-
-
- lib\Decal.dll
- False
-
-
- lib\decalnet.dll
- True
-
-
@@ -401,4 +392,4 @@
-
\ No newline at end of file
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index b78ebb6..ac365f4 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -316,16 +316,7 @@ namespace MosswartMassacre
}
}
- // Auto-update: check for updates 30s after startup
- _updateCheckTimer = new Timer(30000);
- _updateCheckTimer.AutoReset = false;
- _updateCheckTimer.Elapsed += (s, ev) =>
- {
- Task.Run(() => UpdateManager.CheckAndInstallAsync());
- _updateCheckTimer?.Dispose();
- _updateCheckTimer = null;
- };
- _updateCheckTimer.Start();
+ // Auto-update check is initialized after PluginSettings.Initialize() in LoginComplete
}
catch (Exception ex)
@@ -484,6 +475,23 @@ namespace MosswartMassacre
if (WebSocketEnabled)
WebSocket.Start();
+ if (PluginSettings.Instance.AutoUpdateEnabled)
+ {
+ _updateCheckTimer = new Timer(30000);
+ _updateCheckTimer.AutoReset = false;
+ _updateCheckTimer.Elapsed += (s, ev) =>
+ {
+ Task.Run(() => UpdateManager.CheckAndInstallAsync());
+ _updateCheckTimer?.Dispose();
+ _updateCheckTimer = null;
+ };
+ _updateCheckTimer.Start();
+ }
+ else
+ {
+ WriteToChat("[Update] Auto updates disabled in settings");
+ }
+
// Initialize Harmony patches using UtilityBelt's loaded DLL
try
{
@@ -1526,6 +1534,77 @@ namespace MosswartMassacre
WriteToChat("Settings not initialized");
}
}, "Toggle verbose debug logging");
+
+ _commandRouter.Register("metasrefresh", args =>
+ {
+ Task.Run(async () =>
+ {
+ try
+ {
+ var count = await MetaSyncManager.RefreshCatalogAsync();
+ WriteToChat($"[Metas] Loaded {count} remote .met/.nav files");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[Metas] Refresh failed: {ex.Message}");
+ }
+ });
+ }, "Refresh remote metas list");
+
+ _commandRouter.Register("metascheck", args =>
+ {
+ Task.Run(async () =>
+ {
+ try
+ {
+ var summary = await MetaSyncManager.CheckStatusesAsync();
+ WriteToChat($"[Metas] Up-to-date: {summary.upToDate}, Updates: {summary.needsUpdate}, Missing: {summary.notDownloaded}, Errors: {summary.errors}");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[Metas] Check failed: {ex.Message}");
+ }
+ });
+ }, "Check local metas against remote");
+
+ _commandRouter.Register("metaspull", args =>
+ {
+ Task.Run(async () =>
+ {
+ try
+ {
+ var result = await MetaSyncManager.DownloadOutdatedAsync();
+ WriteToChat($"[Metas] Downloaded: {result.downloaded}, Skipped: {result.skipped}, Failed: {result.failed}, Backups: {result.backups}");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[Metas] Pull failed: {ex.Message}");
+ }
+ });
+ }, "Download outdated metas/nav files");
+
+ _commandRouter.Register("metaspullfile", args =>
+ {
+ if (args.Length < 2)
+ {
+ WriteToChat("Usage: /mm metaspullfile ");
+ return;
+ }
+
+ var relativePath = string.Join(" ", args.Skip(1));
+ Task.Run(async () =>
+ {
+ try
+ {
+ var result = await MetaSyncManager.DownloadFileAsync(relativePath);
+ WriteToChat($"[Metas] {relativePath}: {result.Message}");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[Metas] Pull file failed: {ex.Message}");
+ }
+ });
+ }, "Download one metas/nav file by relative path");
}
diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs
index 8104493..c411073 100644
--- a/MosswartMassacre/PluginSettings.cs
+++ b/MosswartMassacre/PluginSettings.cs
@@ -22,11 +22,14 @@ namespace MosswartMassacre
private bool _useTabbedInterface = true;
private string _vtankProfilesPath = "";
private bool _verboseLogging = false;
+ private bool _autoUpdateEnabled = true;
private ChestLooterSettings _chestLooterSettings = new ChestLooterSettings();
public static PluginSettings Instance => _instance
?? throw new InvalidOperationException("PluginSettings not initialized");
+ public static bool IsInitialized => _instance != null;
+
public static void Initialize()
{
// determine plugin folder and character-specific folder
@@ -187,6 +190,12 @@ namespace MosswartMassacre
set { _verboseLogging = value; Save(); }
}
+ public bool AutoUpdateEnabled
+ {
+ get => _autoUpdateEnabled;
+ set { _autoUpdateEnabled = value; Save(); }
+ }
+
public ChestLooterSettings ChestLooterSettings
{
get
diff --git a/MosswartMassacre/ViewXML/mainViewTabbed.xml b/MosswartMassacre/ViewXML/mainViewTabbed.xml
index 2a3a86f..111e4fd 100644
--- a/MosswartMassacre/ViewXML/mainViewTabbed.xml
+++ b/MosswartMassacre/ViewXML/mainViewTabbed.xml
@@ -35,9 +35,12 @@
+
+
+
-
-
+
+
@@ -146,5 +149,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/MosswartMassacre/Views/VVSBaseView.cs b/MosswartMassacre/Views/VVSBaseView.cs
index b61fb5f..2500f6b 100644
--- a/MosswartMassacre/Views/VVSBaseView.cs
+++ b/MosswartMassacre/Views/VVSBaseView.cs
@@ -155,7 +155,7 @@ namespace MosswartMassacre.Views
{
try
{
- if (view != null && PluginSettings.Instance != null)
+ if (view != null && PluginSettings.IsInitialized)
{
PluginSettings.Instance.MainWindowX = view.Location.X;
PluginSettings.Instance.MainWindowY = view.Location.Y;
@@ -171,7 +171,7 @@ namespace MosswartMassacre.Views
{
try
{
- if (view != null && PluginSettings.Instance != null)
+ if (view != null && PluginSettings.IsInitialized)
{
view.Location = new Point(
PluginSettings.Instance.MainWindowX,
@@ -392,4 +392,4 @@ namespace MosswartMassacre.Views
}
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/MosswartMassacre/Views/VVSTabbedMainView.cs b/MosswartMassacre/Views/VVSTabbedMainView.cs
index cccbebc..f1201dc 100644
--- a/MosswartMassacre/Views/VVSTabbedMainView.cs
+++ b/MosswartMassacre/Views/VVSTabbedMainView.cs
@@ -1,5 +1,6 @@
using System;
using System.Drawing;
+using System.Linq;
using System.Timers;
using VirindiViewService.Controls;
@@ -30,6 +31,7 @@ namespace MosswartMassacre.Views
#region Settings Tab Controls
private HudCheckBox chkRareMetaEnabled;
private HudCheckBox chkWebSocketEnabled;
+ private HudCheckBox chkAutoUpdateEnabled;
private HudTextBox txtCharTag;
private HudTextBox txtVTankPath;
#endregion
@@ -70,6 +72,21 @@ namespace MosswartMassacre.Views
private HudStaticText lblLooterStatus;
#endregion
+ #region Metas Tab Controls
+ private HudStaticText lblMetasPath;
+ private HudButton btnRefreshMetasList;
+ private HudButton btnCheckMetasUpdates;
+ private HudButton btnDownloadOutdatedMetas;
+ private HudTextBox txtMetasSearch;
+ private HudButton btnClearMetasSearch;
+ private HudList lstMetasFiles;
+ private HudCombo cmbMetaSelection;
+ private HudButton btnDownloadSelectedMeta;
+ private HudStaticText lblMetasLastCheck;
+ private HudStaticText lblMetasStatus;
+ private string _metasSearchText = string.Empty;
+ #endregion
+
#region Statistics Tracking
private double bestHourlyKills = 0;
private DateTime sessionStartTime;
@@ -149,6 +166,7 @@ namespace MosswartMassacre.Views
InitializeNavigationTabControls();
InitializeFlagTrackerTabControls();
InitializeChestLooterTabControls();
+ InitializeMetasTabControls();
// Initialize the base view and set initial position
Initialize();
@@ -210,6 +228,7 @@ namespace MosswartMassacre.Views
// Settings tab controls
chkRareMetaEnabled = GetControl("chkRareMetaEnabled");
chkWebSocketEnabled = GetControl("chkWebSocketEnabled");
+ chkAutoUpdateEnabled = GetControl("chkAutoUpdateEnabled");
txtCharTag = GetControl("txtCharTag");
txtVTankPath = GetControl("txtVTankPath");
@@ -218,6 +237,8 @@ namespace MosswartMassacre.Views
chkRareMetaEnabled.Change += OnRareMetaSettingChanged;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Change += OnWebSocketSettingChanged;
+ if (chkAutoUpdateEnabled != null)
+ chkAutoUpdateEnabled.Change += OnAutoUpdateSettingChanged;
if (txtCharTag != null)
txtCharTag.Change += OnCharTagChanged;
if (txtVTankPath != null)
@@ -338,6 +359,44 @@ namespace MosswartMassacre.Views
PluginCore.WriteToChat($"Error initializing chest looter controls: {ex.Message}");
}
}
+
+ private void InitializeMetasTabControls()
+ {
+ try
+ {
+ lblMetasPath = GetControl("lblMetasPath");
+ btnRefreshMetasList = GetControl("btnRefreshMetasList");
+ btnCheckMetasUpdates = GetControl("btnCheckMetasUpdates");
+ btnDownloadOutdatedMetas = GetControl("btnDownloadOutdatedMetas");
+ txtMetasSearch = GetControl("txtMetasSearch");
+ btnClearMetasSearch = GetControl("btnClearMetasSearch");
+ lstMetasFiles = GetControl("lstMetasFiles");
+ cmbMetaSelection = GetControl("cmbMetaSelection");
+ btnDownloadSelectedMeta = GetControl("btnDownloadSelectedMeta");
+ lblMetasLastCheck = GetControl("lblMetasLastCheck");
+ lblMetasStatus = GetControl("lblMetasStatus");
+
+ if (btnRefreshMetasList != null)
+ btnRefreshMetasList.Hit += OnRefreshMetasListClick;
+ if (btnCheckMetasUpdates != null)
+ btnCheckMetasUpdates.Hit += OnCheckMetasUpdatesClick;
+ if (btnDownloadOutdatedMetas != null)
+ btnDownloadOutdatedMetas.Hit += OnDownloadOutdatedMetasClick;
+ if (txtMetasSearch != null)
+ txtMetasSearch.Change += OnMetasSearchChanged;
+ if (btnClearMetasSearch != null)
+ btnClearMetasSearch.Hit += OnClearMetasSearchClick;
+ if (btnDownloadSelectedMeta != null)
+ btnDownloadSelectedMeta.Hit += OnDownloadSelectedMetaClick;
+
+ UpdateMetasPathLabel();
+ UpdateMetasStatus("Status: idle");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"Error initializing metas controls: {ex.Message}");
+ }
+ }
#endregion
#region Event Handlers - Settings Tab
@@ -392,6 +451,19 @@ namespace MosswartMassacre.Views
}
}
+ private void OnAutoUpdateSettingChanged(object sender, EventArgs e)
+ {
+ try
+ {
+ PluginSettings.Instance.AutoUpdateEnabled = chkAutoUpdateEnabled.Checked;
+ PluginCore.WriteToChat($"Auto updates {(chkAutoUpdateEnabled.Checked ? "ENABLED" : "DISABLED")}." );
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"Error in auto update setting change: {ex.Message}");
+ }
+ }
+
private void OnVTankPathChanged(object sender, EventArgs e)
{
try
@@ -543,12 +615,14 @@ namespace MosswartMassacre.Views
{
try
{
- if (PluginSettings.Instance != null)
+ if (PluginSettings.IsInitialized)
{
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Checked = PluginSettings.Instance.RareMetaEnabled;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Checked = PluginSettings.Instance.WebSocketEnabled;
+ if (chkAutoUpdateEnabled != null)
+ chkAutoUpdateEnabled.Checked = PluginSettings.Instance.AutoUpdateEnabled;
if (txtCharTag != null)
txtCharTag.Text = PluginSettings.Instance.CharTag ?? "default";
if (txtVTankPath != null)
@@ -579,6 +653,166 @@ namespace MosswartMassacre.Views
}
#endregion
+ #region Event Handlers - Metas Tab
+ private void OnRefreshMetasListClick(object sender, EventArgs e)
+ {
+ try
+ {
+ UpdateMetasPathLabel();
+ UpdateMetasStatus("Status: refreshing remote file list...");
+
+ System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ var count = await MetaSyncManager.RefreshCatalogAsync();
+ PopulateMetasList();
+ PopulateMetasSelection();
+ UpdateMetasLastCheckLabel();
+ UpdateMetasStatus($"Status: loaded {count} remote files");
+ }
+ catch (Exception ex)
+ {
+ UpdateMetasStatus("Status: refresh failed");
+ PluginCore.WriteToChat($"[Metas] Refresh failed: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Refresh command failed: {ex.Message}");
+ }
+ }
+
+ private void OnCheckMetasUpdatesClick(object sender, EventArgs e)
+ {
+ try
+ {
+ UpdateMetasPathLabel();
+ UpdateMetasStatus("Status: checking remote updates...");
+
+ System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ var summary = await MetaSyncManager.CheckStatusesAsync();
+ PopulateMetasList();
+ PopulateMetasSelection();
+ UpdateMetasLastCheckLabel();
+ UpdateMetasStatus($"Status: {summary.upToDate} current, {summary.needsUpdate} updates, {summary.notDownloaded} missing, {summary.errors} errors");
+ }
+ catch (Exception ex)
+ {
+ UpdateMetasStatus("Status: update check failed");
+ PluginCore.WriteToChat($"[Metas] Update check failed: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Update check command failed: {ex.Message}");
+ }
+ }
+
+ private void OnDownloadOutdatedMetasClick(object sender, EventArgs e)
+ {
+ try
+ {
+ UpdateMetasStatus("Status: downloading outdated files...");
+
+ System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ var result = await MetaSyncManager.DownloadOutdatedAsync();
+ PopulateMetasList();
+ PopulateMetasSelection();
+ UpdateMetasLastCheckLabel();
+ UpdateMetasStatus($"Status: downloaded {result.downloaded}, skipped {result.skipped}, failed {result.failed}, backups {result.backups}");
+ }
+ catch (Exception ex)
+ {
+ UpdateMetasStatus("Status: pull outdated failed");
+ PluginCore.WriteToChat($"[Metas] Pull outdated failed: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Pull outdated command failed: {ex.Message}");
+ }
+ }
+
+ private void OnDownloadSelectedMetaClick(object sender, EventArgs e)
+ {
+ try
+ {
+ string selection = GetSelectedMetaRelativePath();
+ if (string.IsNullOrWhiteSpace(selection))
+ {
+ PluginCore.WriteToChat("[Metas] Select a file first");
+ return;
+ }
+
+ UpdateMetasStatus($"Status: downloading {selection}...");
+
+ System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ var result = await MetaSyncManager.DownloadFileAsync(selection);
+ PopulateMetasList();
+ PopulateMetasSelection();
+ UpdateMetasLastCheckLabel();
+ UpdateMetasStatus($"Status: {result.Message}");
+ }
+ catch (Exception ex)
+ {
+ UpdateMetasStatus("Status: download failed");
+ PluginCore.WriteToChat($"[Metas] Download failed: {ex.Message}");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Download command failed: {ex.Message}");
+ }
+ }
+
+ private void OnMetasSearchChanged(object sender, EventArgs e)
+ {
+ try
+ {
+ _metasSearchText = txtMetasSearch?.Text?.Trim() ?? string.Empty;
+ PopulateMetasList();
+ PopulateMetasSelection();
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Search failed: {ex.Message}");
+ }
+ }
+
+ private void OnClearMetasSearchClick(object sender, EventArgs e)
+ {
+ try
+ {
+ _metasSearchText = string.Empty;
+ if (txtMetasSearch != null)
+ {
+ txtMetasSearch.Text = string.Empty;
+ }
+
+ PopulateMetasList();
+ PopulateMetasSelection();
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Clear search failed: {ex.Message}");
+ }
+ }
+ #endregion
+
#region Event Handlers - Flag Tracker Tab
private void OnOpenFlagTrackerClick(object sender, EventArgs e)
{
@@ -732,6 +966,11 @@ namespace MosswartMassacre.Views
{
try
{
+ if (!PluginSettings.IsInitialized)
+ {
+ return;
+ }
+
var settings = PluginSettings.Instance?.ChestLooterSettings;
if (settings != null)
{
@@ -1079,7 +1318,7 @@ namespace MosswartMassacre.Views
{
if (lblAutoLootRare != null)
{
- bool isEnabled = PluginSettings.Instance?.RareMetaEnabled == true;
+ bool isEnabled = PluginSettings.IsInitialized && PluginSettings.Instance.RareMetaEnabled;
if (isEnabled)
{
lblAutoLootRare.Text = "Auto Loot Rare: [ON]";
@@ -1107,7 +1346,7 @@ namespace MosswartMassacre.Views
{
if (lblWebSocketStatus != null)
{
- bool isConnected = PluginSettings.Instance?.WebSocketEnabled == true;
+ bool isConnected = PluginSettings.IsInitialized && PluginSettings.Instance.WebSocketEnabled;
if (isConnected)
{
lblWebSocketStatus.Text = "WebSocket: [CONNECTED]";
@@ -1182,6 +1421,264 @@ namespace MosswartMassacre.Views
PluginCore.WriteToChat($"Error populating nav file dropdown: {ex.Message}");
}
}
+
+ private void PopulateMetasList()
+ {
+ try
+ {
+ if (lstMetasFiles == null)
+ {
+ return;
+ }
+
+ lstMetasFiles.ClearRows();
+ var files = ApplyMetasSearch(MetaSyncManager.GetCachedFiles());
+
+ if (files == null || files.Count == 0)
+ {
+ var emptyRow = lstMetasFiles.AddRow();
+ SafeSetListText(emptyRow, 0, "-");
+ SafeSetListText(emptyRow, 1, string.IsNullOrWhiteSpace(_metasSearchText) ? "No files loaded" : "No matches");
+ SafeSetListText(emptyRow, 2, string.IsNullOrWhiteSpace(_metasSearchText) ? "Click Refresh List" : "Try a different filter");
+ return;
+ }
+
+ foreach (var file in files)
+ {
+ var row = lstMetasFiles.AddRow();
+ SafeSetListText(row, 0, file.Extension?.ToUpperInvariant() ?? "");
+ SafeSetListText(row, 1, file.RelativePath ?? file.FileName ?? "");
+ SafeSetListText(row, 2, file.StatusText ?? "Unknown");
+
+ Color color;
+ switch (file.Status)
+ {
+ case MetaFileSyncStatus.UpToDate:
+ color = Color.FromArgb(0, 128, 0);
+ break;
+ case MetaFileSyncStatus.UpdateAvailable:
+ color = Color.FromArgb(180, 110, 0);
+ break;
+ case MetaFileSyncStatus.NotDownloaded:
+ color = Color.FromArgb(0, 70, 160);
+ break;
+ case MetaFileSyncStatus.Error:
+ color = Color.FromArgb(180, 0, 0);
+ break;
+ default:
+ color = Color.White;
+ break;
+ }
+
+ SafeSetListColor(row, 2, color);
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Error populating list: {ex.Message}");
+ }
+ }
+
+ private void PopulateMetasSelection()
+ {
+ try
+ {
+ if (cmbMetaSelection == null)
+ {
+ return;
+ }
+
+ var previouslySelected = GetSelectedMetaRelativePath();
+ var files = ApplyMetasSearch(MetaSyncManager.GetCachedFiles())
+ .OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ cmbMetaSelection.Clear();
+
+ foreach (var file in files)
+ {
+ var label = $"[{file.Extension}] {file.RelativePath}";
+ cmbMetaSelection.AddItem(label, file.RelativePath);
+ }
+
+ if (cmbMetaSelection.Count > 0)
+ {
+ var selectedIndex = 0;
+ if (!string.IsNullOrWhiteSpace(previouslySelected))
+ {
+ for (int i = 0; i < cmbMetaSelection.Count; i++)
+ {
+ var value = ((HudStaticText)cmbMetaSelection[i]).Text;
+ if (!string.IsNullOrWhiteSpace(value) && value.EndsWith(previouslySelected, StringComparison.OrdinalIgnoreCase))
+ {
+ selectedIndex = i;
+ break;
+ }
+ }
+ }
+
+ cmbMetaSelection.Current = selectedIndex;
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Metas] Error populating selection: {ex.Message}");
+ }
+ }
+
+ private string GetSelectedMetaRelativePath()
+ {
+ try
+ {
+ if (cmbMetaSelection == null || cmbMetaSelection.Count == 0 || cmbMetaSelection.Current < 0 || cmbMetaSelection.Current >= cmbMetaSelection.Count)
+ {
+ return string.Empty;
+ }
+
+ var text = ((HudStaticText)cmbMetaSelection[cmbMetaSelection.Current]).Text ?? string.Empty;
+ var closeBracket = text.IndexOf(']');
+ if (closeBracket >= 0 && closeBracket + 2 < text.Length)
+ {
+ return text.Substring(closeBracket + 2).Trim();
+ }
+
+ return text.Trim();
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ private void UpdateMetasPathLabel()
+ {
+ try
+ {
+ if (lblMetasPath == null)
+ {
+ return;
+ }
+
+ var path = MetaSyncManager.GetResolvedVtankPath();
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ lblMetasPath.Text = "VTank Path: unresolved";
+ }
+ else
+ {
+ lblMetasPath.Text = $"VTank Path: {path}";
+ }
+ }
+ catch
+ {
+ if (lblMetasPath != null)
+ {
+ lblMetasPath.Text = "VTank Path: unresolved";
+ }
+ }
+ }
+
+ private void UpdateMetasLastCheckLabel()
+ {
+ try
+ {
+ if (lblMetasLastCheck == null)
+ {
+ return;
+ }
+
+ var ts = MetaSyncManager.LastStatusCheckUtc != DateTime.MinValue
+ ? MetaSyncManager.LastStatusCheckUtc.ToLocalTime()
+ : MetaSyncManager.LastCatalogRefreshUtc.ToLocalTime();
+
+ if (ts == DateTime.MinValue)
+ {
+ lblMetasLastCheck.Text = "Last Check: never";
+ }
+ else
+ {
+ lblMetasLastCheck.Text = $"Last Check: {ts:yyyy-MM-dd HH:mm:ss}";
+ }
+ }
+ catch
+ {
+ if (lblMetasLastCheck != null)
+ {
+ lblMetasLastCheck.Text = "Last Check: unknown";
+ }
+ }
+ }
+
+ private void UpdateMetasStatus(string text)
+ {
+ try
+ {
+ if (lblMetasStatus != null)
+ {
+ lblMetasStatus.Text = text ?? "Status: idle";
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private System.Collections.Generic.IReadOnlyList ApplyMetasSearch(System.Collections.Generic.IReadOnlyList files)
+ {
+ if (files == null || files.Count == 0)
+ {
+ return files;
+ }
+
+ var filter = _metasSearchText?.Trim();
+ if (string.IsNullOrWhiteSpace(filter))
+ {
+ return files;
+ }
+
+ return files
+ .Where(f => (!string.IsNullOrWhiteSpace(f.RelativePath) && f.RelativePath.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
+ || (!string.IsNullOrWhiteSpace(f.FileName) && f.FileName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0))
+ .ToList();
+ }
+
+ private void SafeSetListText(HudList.HudListRowAccessor row, int columnIndex, string text)
+ {
+ try
+ {
+ if (row != null && columnIndex >= 0)
+ {
+ try
+ {
+ var control = row[columnIndex];
+ if (control != null)
+ {
+ ((HudStaticText)control).Text = text ?? string.Empty;
+ }
+ }
+ catch
+ {
+ }
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private void SafeSetListColor(HudList.HudListRowAccessor row, int columnIndex, Color color)
+ {
+ try
+ {
+ if (row != null && columnIndex >= 0 && row[columnIndex] != null)
+ {
+ ((HudStaticText)row[columnIndex]).TextColor = color;
+ }
+ }
+ catch
+ {
+ }
+ }
#endregion
#region IDisposable Override
@@ -1198,6 +1695,8 @@ namespace MosswartMassacre.Views
chkRareMetaEnabled.Change -= OnRareMetaSettingChanged;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Change -= OnWebSocketSettingChanged;
+ if (chkAutoUpdateEnabled != null)
+ chkAutoUpdateEnabled.Change -= OnAutoUpdateSettingChanged;
if (txtCharTag != null)
txtCharTag.Change -= OnCharTagChanged;
if (txtVTankPath != null)
@@ -1220,6 +1719,19 @@ namespace MosswartMassacre.Views
btnLoadRoute.Hit -= OnLoadRouteClick;
if (btnClearRoute != null)
btnClearRoute.Hit -= OnClearRouteClick;
+
+ if (btnRefreshMetasList != null)
+ btnRefreshMetasList.Hit -= OnRefreshMetasListClick;
+ if (btnCheckMetasUpdates != null)
+ btnCheckMetasUpdates.Hit -= OnCheckMetasUpdatesClick;
+ if (btnDownloadOutdatedMetas != null)
+ btnDownloadOutdatedMetas.Hit -= OnDownloadOutdatedMetasClick;
+ if (txtMetasSearch != null)
+ txtMetasSearch.Change -= OnMetasSearchChanged;
+ if (btnClearMetasSearch != null)
+ btnClearMetasSearch.Hit -= OnClearMetasSearchClick;
+ if (btnDownloadSelectedMeta != null)
+ btnDownloadSelectedMeta.Hit -= OnDownloadSelectedMetaClick;
// No enhanced events to clean up
}
@@ -1234,4 +1746,4 @@ namespace MosswartMassacre.Views
}
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index aec25fa..4eb7303 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ