diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index 1c081fe..93891ca 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -179,6 +179,7 @@ + True diff --git a/MosswartMassacre/MossyInventory.cs b/MosswartMassacre/MossyInventory.cs index c99cd6b..299b130 100644 --- a/MosswartMassacre/MossyInventory.cs +++ b/MosswartMassacre/MossyInventory.cs @@ -209,7 +209,7 @@ namespace MosswartMassacre { return; // Settings not ready, skip silently } - DumpInventoryToFile(); + DumpInventoryToFile(true); // Request IDs if missing to ensure complete data } catch (Exception ex) { @@ -293,5 +293,44 @@ namespace MosswartMassacre || (oc == ObjectClass.Gem && !string.IsNullOrEmpty(name) && name.Contains("Aetheria")) || (oc == ObjectClass.Misc && !string.IsNullOrEmpty(name) && name.Contains("Essence")); } + + /// + /// Forces an inventory upload with ID requests - guarantees complete data + /// + public void ForceInventoryUpload() + { + try + { + // Check if inventory logging is enabled + try + { + if (!PluginSettings.Instance.InventoryLog) + { + PluginCore.WriteToChat("[INV] Inventory logging is disabled"); + return; + } + } + catch (InvalidOperationException) + { + PluginCore.WriteToChat("[INV] Settings not ready"); + return; + } + + // Check if WebSocket is enabled + if (!PluginCore.WebSocketEnabled) + { + PluginCore.WriteToChat("[INV] WebSocket streaming is disabled"); + return; + } + + PluginCore.WriteToChat("[INV] Forcing inventory upload with ID requests..."); + DumpInventoryToFile(true); // Request IDs if missing + PluginCore.WriteToChat("[INV] Inventory upload completed"); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[INV] Force upload failed: {ex.Message}"); + } + } } } diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 9c3579a..9e77418 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -104,6 +104,11 @@ namespace MosswartMassacre { Views.VVSTabbedMainView.RefreshSettingsFromConfig(); } + + public static void RefreshUpdateStatus() + { + Views.VVSTabbedMainView.RefreshUpdateStatus(); + } } public static bool RemoteCommandsEnabled { get; set; } = false; public static bool HttpServerEnabled { get; set; } = false; @@ -227,7 +232,9 @@ namespace MosswartMassacre PluginSettings.Save(); if (TelemetryEnabled) Telemetry.Stop(); // ensure no dangling timer / HttpClient - WriteToChat("Mosswart Massacre is shutting down!!!!!"); + WriteToChat("Mosswart Massacre is shutting down. Bye!"); + + // Unsubscribe from chat message event CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText); @@ -1278,6 +1285,10 @@ namespace MosswartMassacre WriteToChat("/mm testtaper - Test cached Prismatic Taper tracking"); WriteToChat("/mm debugtaper - Show detailed taper tracking debug info"); WriteToChat("/mm gui - Manually initialize/reinitialize GUI!!!"); + WriteToChat("/mm checkforupdate - Check for plugin updates"); + WriteToChat("/mm update - Download and install update (if available)"); + WriteToChat("/mm debugupdate - Debug update UI controls"); + WriteToChat("/mm sendinventory - Force inventory upload with ID requests"); break; case "report": TimeSpan elapsed = DateTime.Now - statsStartTime; @@ -1655,6 +1666,46 @@ namespace MosswartMassacre } break; + case "checkforupdate": + // Run the update check asynchronously + Task.Run(async () => + { + await UpdateManager.CheckForUpdateAsync(); + // Update UI if available + try + { + ViewManager.RefreshUpdateStatus(); + } + catch (Exception ex) + { + WriteToChat($"Error refreshing UI: {ex.Message}"); + } + }); + break; + + case "update": + // Run the update installation asynchronously + Task.Run(async () => + { + await UpdateManager.DownloadAndInstallUpdateAsync(); + }); + break; + + case "debugupdate": + Views.VVSTabbedMainView.DebugUpdateControls(); + break; + + case "sendinventory": + // Force inventory upload with ID requests + if (_inventoryLogger != null) + { + _inventoryLogger.ForceInventoryUpload(); + } + else + { + WriteToChat("[INV] Inventory system not initialized"); + } + break; default: WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help"); diff --git a/MosswartMassacre/UpdateManager.cs b/MosswartMassacre/UpdateManager.cs new file mode 100644 index 0000000..af14e60 --- /dev/null +++ b/MosswartMassacre/UpdateManager.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace MosswartMassacre +{ + public static class UpdateManager + { + private const string UPDATE_URL = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll"; + + private static bool updateAvailable = false; + private static long remoteFileSize = 0; + private static long localFileSize = 0; + private static DateTime lastCheckTime = DateTime.MinValue; + + public static bool IsUpdateAvailable => updateAvailable; + public static DateTime LastCheckTime => lastCheckTime; + + public static async Task CheckForUpdateAsync() + { + try + { + PluginCore.WriteToChat("[Update] Checking for updates..."); + + // Get local file size + string localPath = GetLocalDllPath(); + if (!File.Exists(localPath)) + { + PluginCore.WriteToChat("[Update] Error: Could not find local DLL file"); + return false; + } + + localFileSize = new FileInfo(localPath).Length; + + // Check remote file size + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromSeconds(10); + + var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, UPDATE_URL)); + response.EnsureSuccessStatusCode(); + + remoteFileSize = response.Content.Headers.ContentLength ?? 0; + + if (remoteFileSize == 0) + { + PluginCore.WriteToChat("[Update] Error: Could not determine remote file size"); + return false; + } + } + + // Compare sizes + updateAvailable = (remoteFileSize != localFileSize); + lastCheckTime = DateTime.Now; + + if (updateAvailable) + { + PluginCore.WriteToChat($"[Update] Update available! Local: {localFileSize} bytes, Remote: {remoteFileSize} bytes"); + } + else + { + PluginCore.WriteToChat("[Update] Up to date"); + } + + return true; + } + catch (HttpRequestException ex) + { + PluginCore.WriteToChat($"[Update] Network error: {ex.Message}"); + return false; + } + catch (TaskCanceledException) + { + PluginCore.WriteToChat("[Update] Request timed out"); + return false; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[Update] Check failed: {ex.Message}"); + return false; + } + } + + public static async Task DownloadAndInstallUpdateAsync() + { + if (!updateAvailable) + { + PluginCore.WriteToChat("[Update] No update available. Run /mm checkforupdate first."); + return false; + } + + try + { + PluginCore.WriteToChat("[Update] Downloading update..."); + + string localPath = GetLocalDllPath(); + string tempPath = localPath + ".tmp"; + string backupPath = localPath + ".bak"; + + // Download to temp file + using (var client = new HttpClient()) + { + client.Timeout = TimeSpan.FromSeconds(30); + + var response = await client.GetAsync(UPDATE_URL); + response.EnsureSuccessStatusCode(); + + using (var fileStream = File.Create(tempPath)) + { + await response.Content.CopyToAsync(fileStream); + } + } + + // Validate downloaded file + var downloadedSize = new FileInfo(tempPath).Length; + if (downloadedSize != remoteFileSize) + { + File.Delete(tempPath); + PluginCore.WriteToChat($"[Update] Download validation failed. Expected {remoteFileSize} bytes, got {downloadedSize} bytes"); + return false; + } + + PluginCore.WriteToChat("[Update] Download complete, installing..."); + + // Atomically replace current file with new version (creates backup automatically) + File.Replace(tempPath, localPath, backupPath); + + // Clear update flag + updateAvailable = false; + + PluginCore.WriteToChat("[Update] Update installed successfully!"); + PluginCore.WriteToChat("[Update] Previous version backed up as MosswartMassacre.dll.bak"); + + // Wait a moment for file system to settle, then trigger hot reload + await System.Threading.Tasks.Task.Delay(1000); + + try + { + // Touch the file to ensure FileSystemWatcher detects the change + File.SetLastWriteTime(localPath, DateTime.Now); + PluginCore.WriteToChat("[Update] Triggering hot reload..."); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[Update] Could not trigger hot reload: {ex.Message}"); + PluginCore.WriteToChat("[Update] Please use /mm gui to reload manually"); + } + + return true; + } + catch (HttpRequestException ex) + { + PluginCore.WriteToChat($"[Update] Download error: {ex.Message}"); + return false; + } + catch (TaskCanceledException) + { + PluginCore.WriteToChat("[Update] Download timed out"); + return false; + } + catch (UnauthorizedAccessException) + { + PluginCore.WriteToChat("[Update] File access denied. Make sure the plugin directory is writable."); + return false; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"[Update] Install failed: {ex.Message}"); + return false; + } + } + + private static string GetLocalDllPath() + { + // Get the path to the current DLL + string assemblyPath = typeof(PluginCore).Assembly.Location; + + // If empty (hot reload scenario), use AssemblyDirectory + filename + if (string.IsNullOrEmpty(assemblyPath)) + { + return Path.Combine(PluginCore.AssemblyDirectory, "MosswartMassacre.dll"); + } + + return assemblyPath; + } + + public static string GetUpdateStatus() + { + if (lastCheckTime == DateTime.MinValue) + { + return "Update Status: Not checked"; + } + + return updateAvailable ? "Update Status: Update available" : "Update Status: Up to date"; + } + } +} \ No newline at end of file diff --git a/MosswartMassacre/ViewXML/mainViewTabbed.xml b/MosswartMassacre/ViewXML/mainViewTabbed.xml index ada1e9f..a0a7563 100644 --- a/MosswartMassacre/ViewXML/mainViewTabbed.xml +++ b/MosswartMassacre/ViewXML/mainViewTabbed.xml @@ -16,6 +16,11 @@ + + + + + diff --git a/MosswartMassacre/Views/VVSTabbedMainView.cs b/MosswartMassacre/Views/VVSTabbedMainView.cs index ce2b661..bbaf284 100644 --- a/MosswartMassacre/Views/VVSTabbedMainView.cs +++ b/MosswartMassacre/Views/VVSTabbedMainView.cs @@ -22,6 +22,9 @@ namespace MosswartMassacre.Views private HudStaticText lblAutoLootRare; private HudStaticText lblStatus; private HudStaticText lblWebSocketStatus; + private HudButton btnCheckUpdate; + private HudButton btnInstallUpdate; + private HudStaticText lblUpdateStatus; #endregion #region Settings Tab Controls @@ -169,11 +172,21 @@ namespace MosswartMassacre.Views lblAutoLootRare = GetControl("lblAutoLootRare"); lblStatus = GetControl("lblStatus"); lblWebSocketStatus = GetControl("lblWebSocketStatus"); + btnCheckUpdate = GetControl("btnCheckUpdate"); + btnInstallUpdate = GetControl("btnInstallUpdate"); + lblUpdateStatus = GetControl("lblUpdateStatus"); + + // Hook up update button events + if (btnCheckUpdate != null) + btnCheckUpdate.Hit += OnCheckUpdateClicked; + if (btnInstallUpdate != null) + btnInstallUpdate.Hit += OnInstallUpdateClicked; // Update status displays immediately UpdateStatus(); UpdateAutoLootRareIndicator(); UpdateWebSocketStatus(); + RefreshUpdateStatus(); } catch (Exception ex) { @@ -407,6 +420,134 @@ namespace MosswartMassacre.Views } } + private void OnCheckUpdateClicked(object sender, EventArgs e) + { + try + { + // Run update check asynchronously + System.Threading.Tasks.Task.Run(async () => + { + await UpdateManager.CheckForUpdateAsync(); + // Update UI (VVS controls are thread-safe for simple property updates) + try + { + RefreshUpdateStatus(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error updating UI after check: {ex.Message}"); + } + }); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error checking for updates: {ex.Message}"); + } + } + + private void OnInstallUpdateClicked(object sender, EventArgs e) + { + try + { + // Check if update check was performed first + if (UpdateManager.LastCheckTime == DateTime.MinValue) + { + PluginCore.WriteToChat("[Update] Check for updates first"); + return; + } + + if (!UpdateManager.IsUpdateAvailable) + { + PluginCore.WriteToChat("[Update] No update available"); + return; + } + + // Run update installation asynchronously + System.Threading.Tasks.Task.Run(async () => + { + await UpdateManager.DownloadAndInstallUpdateAsync(); + // Update UI (VVS controls are thread-safe for simple property updates) + try + { + RefreshUpdateStatus(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error updating UI after install: {ex.Message}"); + } + }); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error installing update: {ex.Message}"); + } + } + + public static void RefreshUpdateStatus() + { + try + { + if (instance?.lblUpdateStatus != null) + { + if (UpdateManager.LastCheckTime == DateTime.MinValue) + { + // Not checked yet + instance.lblUpdateStatus.Text = ""; + } + else if (UpdateManager.IsUpdateAvailable) + { + // Update available + instance.lblUpdateStatus.Text = "Update available"; + } + else + { + // Up to date + instance.lblUpdateStatus.Text = "Up to date"; + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing update status: {ex.Message}"); + } + } + + public void RefreshUpdateStatusInstance() + { + RefreshUpdateStatus(); + } + + public static void DebugUpdateControls() + { + try + { + PluginCore.WriteToChat("=== Update UI Debug (Main Tab) ==="); + PluginCore.WriteToChat($"Instance exists: {instance != null}"); + if (instance != null) + { + PluginCore.WriteToChat($"btnCheckUpdate: {instance.btnCheckUpdate != null}"); + PluginCore.WriteToChat($"btnInstallUpdate: {instance.btnInstallUpdate != null}"); + PluginCore.WriteToChat($"lblUpdateStatus: {instance.lblUpdateStatus != null}"); + + if (instance.lblUpdateStatus != null) + { + PluginCore.WriteToChat($"Current status text: '{instance.lblUpdateStatus.Text}'"); + } + + PluginCore.WriteToChat($"UpdateManager.LastCheckTime: {UpdateManager.LastCheckTime}"); + PluginCore.WriteToChat($"UpdateManager.IsUpdateAvailable: {UpdateManager.IsUpdateAvailable}"); + + // Try to force refresh + RefreshUpdateStatus(); + PluginCore.WriteToChat("Forced refresh completed"); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug error: {ex.Message}"); + } + } + private void LoadCurrentSettings() { try @@ -875,6 +1016,10 @@ namespace MosswartMassacre.Views txtCharTag.Change -= OnCharTagChanged; if (txtVTankPath != null) txtVTankPath.Change -= OnVTankPathChanged; + if (btnCheckUpdate != null) + btnCheckUpdate.Hit -= OnCheckUpdateClicked; + if (btnInstallUpdate != null) + btnInstallUpdate.Hit -= OnInstallUpdateClicked; // Statistics tab event cleanup if (btnResetStats != null) diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index 67e9df1..85f8e13 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ diff --git a/MosswartMassacre/packages.config b/MosswartMassacre/packages.config index 62f1f9d..bcad711 100644 --- a/MosswartMassacre/packages.config +++ b/MosswartMassacre/packages.config @@ -1,5 +1,7 @@  + + \ No newline at end of file