diff --git a/.gitignore b/.gitignore
index 8cb9ff0..9491a2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,14 +360,4 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
-/UI_Creation_Manual.md
-/UI_Creation_VirindiViewService_Manual.md
-/MosswartMassacre/Unused
-/MosswartMassacre/Decal.Adapter.csproj
-/MosswartMassacre/Decal.Interop.Core.csproj
-
-# Claude Code
-.claude/
-CLAUDE.md
-**/CLAUDE.md
+FodyWeavers.xsd
\ No newline at end of file
diff --git a/GearCycler/GearCore.cs b/GearCycler/GearCore.cs
new file mode 100644
index 0000000..09b380a
--- /dev/null
+++ b/GearCycler/GearCore.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Decal.Adapter;
+using Decal.Adapter.Wrappers;
+using VirindiViewService;
+using VirindiViewService.Controls;
+
+namespace GearCycler
+{
+ [ComVisible(true)]
+ [Guid("9b6a07e1-ae78-47f4-b09c-174f6a27d7a3")] // Replace with your own unique GUID if needed
+ [FriendlyName("GearCycler")]
+ public class GearCore : PluginBase
+ {
+ public HudView view;
+ private HudButton btnCycle;
+
+ protected override void Startup()
+ {
+ try
+ {
+ string xml = File.ReadAllText("ViewXML\\mainview.xml");
+ view = HudView.ReadXmlLayout(xml);
+ view.Visible = true;
+
+ btnCycle = (HudButton)view.Controls["btnCycle"];
+ btnCycle.Hit += (s, e) =>
+ {
+ CoreManager.Current.Actions.AddChatText("[GearCycler] Button clicked!", 1);
+ };
+ }
+ catch (Exception ex)
+ {
+ CoreManager.Current.Actions.AddChatText($"[GearCycler] Failed to load UI: {ex.Message}", 1);
+ }
+ }
+
+ protected override void Shutdown()
+ {
+ btnCycle?.Dispose();
+ view?.Dispose();
+ }
+ }
+}
diff --git a/GearCycler/GearCycler.csproj b/GearCycler/GearCycler.csproj
new file mode 100644
index 0000000..b4344de
--- /dev/null
+++ b/GearCycler/GearCycler.csproj
@@ -0,0 +1,81 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {1293560E-2A56-417F-8116-8CE0420DC97C}
+ Library
+ Properties
+ GearCycler
+ GearCycler
+ v4.8
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ TRACE;DEBUG;VVS_REFERENCED;DECAL_INTEROP
+ prompt
+ 4
+ x86
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\MosswartMassacre\lib\Decal.Adapter.dll
+
+
+ False
+ True
+ ..\MosswartMassacre\lib\Decal.Interop.Core.DLL
+
+
+ False
+ True
+ ..\MosswartMassacre\lib\Decal.Interop.Inject.dll
+
+
+
+
+
+
+
+
+
+
+
+ ..\MosswartMassacre\lib\VirindiViewService.dll
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/GearCycler/Properties/AssemblyInfo.cs b/GearCycler/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..c45a3b8
--- /dev/null
+++ b/GearCycler/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("GearCycler")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("GearCycler")]
+[assembly: AssemblyCopyright("Copyright © 2025")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f5462318-d26a-4ab0-8981-80edd9ec9c99")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/GearCycler/Properties/Resources.Designer.cs b/GearCycler/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..40b9adf
--- /dev/null
+++ b/GearCycler/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace GearCycler.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GearCycler.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/GearCycler/Properties/Resources.resx b/GearCycler/Properties/Resources.resx
new file mode 100644
index 0000000..4fdb1b6
--- /dev/null
+++ b/GearCycler/Properties/Resources.resx
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/GearCycler/ViewXML/mainView.xml b/GearCycler/ViewXML/mainView.xml
new file mode 100644
index 0000000..1300d02
--- /dev/null
+++ b/GearCycler/ViewXML/mainView.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/GearCycler/lib/Decal.Adapter.dll b/GearCycler/lib/Decal.Adapter.dll
new file mode 100644
index 0000000..c27e132
Binary files /dev/null and b/GearCycler/lib/Decal.Adapter.dll differ
diff --git a/GearCycler/lib/Decal.Interop.Core.DLL b/GearCycler/lib/Decal.Interop.Core.DLL
new file mode 100644
index 0000000..b8de808
Binary files /dev/null and b/GearCycler/lib/Decal.Interop.Core.DLL differ
diff --git a/GearCycler/lib/Decal.Interop.Inject.dll b/GearCycler/lib/Decal.Interop.Inject.dll
new file mode 100644
index 0000000..f186c93
Binary files /dev/null and b/GearCycler/lib/Decal.Interop.Inject.dll differ
diff --git a/GearCycler/lib/VirindiViewService.dll b/GearCycler/lib/VirindiViewService.dll
new file mode 100644
index 0000000..1e3f4fc
Binary files /dev/null and b/GearCycler/lib/VirindiViewService.dll differ
diff --git a/GearCycler/lib/VirindiViewService.xml b/GearCycler/lib/VirindiViewService.xml
new file mode 100644
index 0000000..c43230e
--- /dev/null
+++ b/GearCycler/lib/VirindiViewService.xml
@@ -0,0 +1,386 @@
+
+
+
+ VirindiViewService
+
+
+
+
+ Implies Top and Left
+
+
+
+
+ Provides theme elements, which can be drawn by controls.
+
+
+
+
+ Displays an element from the current theme.
+
+
+
+
+ The base class for all Virindi Views controls.
+
+
+
+
+ Called after this control is added to a ControlGroup. This is when the Name and details have been set.
+
+
+
+
+ Add and initialize a child control of this control. The child may be removed by disposing it.
+
+
+
+
+
+ Called when a child of this control is disposed.
+
+
+
+
+
+ Recursively disposes all children and removes this control from the view, if it is initialized.
+
+
+
+
+ Handles a mouse wheel event. Parent controls must pass this on to applicable children if necessary.
+
+
+
+
+
+
+ Fires the MouseEvent event for mouse down, and sets this control as the focus control if CanTakeFocus is true.
+
+ Parent controls must pass this on to applicable children if necessary.
+
+
+
+
+
+ Fires the MouseEvent event for mouse up as well as the Hit event.
+
+ Parent controls must pass this on to applicable children if necessary.
+
+
+
+
+
+
+ Fired when the mousedown originated outside the current view. The base version of this method
+ passes on the event to all children if the 'up' point is within its saved rect.
+
+ Mouseup point
+
+
+
+ Tracks mouseover and fires the MouseOverChange event, as well as the MouseEvent event for mouse move.
+
+ Parent controls must pass this on to applicable children if necessary.
+
+
+
+
+
+ Parses a key message and fires the specific key event methods.
+
+ Key events are only sent to the control with focus.
+
+
+
+
+
+
+
+
+ WARNING: ONLY A PARENT CONTROL SHOULD CALL THIS METHOD.
+
+ This method is overridden in derived controls to handle the actual control drawing. Overridden methods should call
+ the base, draw, and recursively call this method on all child controls.
+
+
+
+
+
+ WARNING: ONLY A PARENT CONTROL SHOULD CALL THIS METHOD.
+
+ Notifies a control of changed saved draw options. This method saves its parameters in the Savedxxx properties.
+ Parent controls should override this method and recursively notify children of their new draw options, altering
+ their pClipRegion to reflect their new position in the View.
+
+ This base method also fires the DrawStateChange and ThemeChanged events.
+
+ This control's area, relative to the view area.
+ The theme applied to this control.
+ The context of this control, eg. inside a listbox.
+ The position of the View, in game window coordinates.
+
+
+
+ WARNING: ONLY A PARENT CONTROL SHOULD SET THIS PROPERTY.
+
+
+
+
+ List of XmlAttributes present on the XmlNode that was used to construct this control, if the control was loaded from XML. Otherwise, empty.
+
+
+
+
+ The XmlNode used to construct this control, if the control was loaded from XML. Otherwise, null.
+
+
+
+
+ The name that this control will be initialized with.
+
+
+
+
+ A multiline uneditable scrolling text box.
+
+
+
+
+ A single image control.
+
+
+
+
+ A button using custom images.
+
+
+
+
+ A doubly-linked list with a Dictionary index. Duplicate items are not allowed.
+ -Add is O(1)
+ -Contains is O(1)
+ -Remove is O(1)
+ -Get/set by index is O(n)
+ -Insert is O(n)
+ -RemoveAt is O(n)
+ Additionally, a cached pointer (with associated index) is kept pointing to the last used index item.
+ When looking up an item by index, the list is walked from the head, tail, or cached index pointer.
+ Thus, doing multiple operations in index order is O(1) even without an enumerator.
+
+
+
+
+
+ This method gets the node corresponding to a particular index. To get there,
+ the list is traversed from the head, tail, or cached index pointer (if valid).
+
+
+
+
+
+
+ Web browser control, using Awesomium (free license version)
+
+
+
+
+ A horizontal scrollbar.
+
+
+
+
+ Summary description for ByteCursor.
+
+
+
+
+ A checkbox with optional associated text. Uses its parent to provide the background.
+
+
+
+
+ A single-line text input box.
+
+
+
+
+ Called before render so the required size of the new target area can be calculated.
+ The returned value is the size of the desired draw area, not including outer borders and
+ style-dependent padding. This size must be less than or equal to MaximumSize in each dimension.
+
+
+
+
+
+
+ Draw this element. When this is called, the background and borders will already have been drawn, and
+ target will already be in BeginRender. This method should leave the target in render mode.
+
+
+
+
+
+
+
+ A renderer for string-only tooltips.
+
+
+
+
+ Represents an unordered set of items. Duplicates are not allowed.
+ (This is really just a dictionary which only holds keys.)
+ Should be used when a collection of non-duplicate items is needed and
+ the order doesn't matter.
+
+
+
+
+ A series of titled tabs along the top, each one having an associated control which appears
+ on the bottom when its tab is enabled.
+
+
+
+
+ A progressbar.
+
+
+
+
+ A regular pushbutton-style control.
+
+
+
+
+ Calls the non-hooked IDirect3DDevice9::BeginScene function. When rendering inside a VVS view or texture, use DxTexture.BeginRender() instead.
+
+
+
+
+
+ Calls the non-hooked IDirect3DDevice9::EndScene function. When rendering inside a VVS view or texture, use DxTexture.EndRender() instead.
+
+
+
+
+
+ Gets the current instance of the VVS bar.
+
+
+
+
+ A console containing game chat.
+
+
+
+
+ Initializes Direct3D drawing and sets the rendertarget to this texture. Calls to this method should be minimized to improve performance. DxTexture.EndRender() must be called after calling this method.
+
+
+
+
+
+
+
+
+
+ Ends Direct3D rendering and resets the rendertarget. Must be called after DxTexture.BeginRender().
+
+
+
+
+ Note: Before use, FlushSprite() may need to be called to ensure correct ordering.
+
+
+
+
+
+
+
+
+ Note: Before use, you must call BeginUserDrawOperation().
+
+
+
+
+
+
+
+
+ Note: Before use, you must call BeginUserDrawOperation().
+
+
+
+
+
+
+
+
+
+ A vertically scrolling list, containing a number of rows and columns. Every row
+ has the same number and types of columns. Each column contains a specified control type.
+
+
+
+
+ A number of images on top of each other, which always draw in the proper order.
+
+
+
+
+ A simple text display control. Uses its parent to provide the background.
+
+
+
+
+ A container for multiple controls with set locations and sizes within.
+
+
+
+
+ A dropdown list.
+
+
+
+
+ If the context menu is not visible, it is created at the specified point.
+
+
+
+
+ If the context menu is not visible, it is created at the specified point with the specified theme.
+
+
+
+
+ Provides information about an associated tooltip.
+
+
+
+
+ The HudControl that the tip is attached to.
+
+
+
+
+ Deprecated.
+ Returns the text associated with a tooltip only if the tip contains a cStringRenderer.
+
+
+
+
+ A vertical scrollbar.
+
+
+
+
+ A horizontal slider.
+
+
+
+
+ A control that allows easy access to underlying draw methods.
+
+
+
+
diff --git a/GearCycler/mainView.xml b/GearCycler/mainView.xml
new file mode 100644
index 0000000..d68a2a3
--- /dev/null
+++ b/GearCycler/mainView.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/MosswartMassacre.Loader/LoaderCore.cs b/MosswartMassacre.Loader/LoaderCore.cs
deleted file mode 100644
index 4aae64c..0000000
--- a/MosswartMassacre.Loader/LoaderCore.cs
+++ /dev/null
@@ -1,264 +0,0 @@
-using System;
-using System.IO;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using Microsoft.Win32;
-using Decal.Adapter;
-
-namespace MosswartMassacre.Loader
-{
- [FriendlyName("MosswartMassacre.Loader")]
- public class LoaderCore : FilterBase
- {
- private Assembly pluginAssembly;
- private Type pluginType;
- private object pluginInstance;
- private FileSystemWatcher pluginWatcher;
- private bool isSubscribedToRenderFrame = false;
- private bool needsReload;
-
- public static string PluginAssemblyNamespace => "MosswartMassacre";
- public static string PluginAssemblyName => $"{PluginAssemblyNamespace}.dll";
- public static string PluginAssemblyGuid => "{8C97E839-4D05-4A5F-B0C8-E8E778654322}";
-
- public static bool IsPluginLoaded { get; private set; }
-
- ///
- /// Assembly directory (contains both loader and plugin dlls)
- ///
- public static string AssemblyDirectory => System.IO.Path.GetDirectoryName(Assembly.GetAssembly(typeof(LoaderCore)).Location);
-
- public DateTime LastDllChange { get; private set; }
-
- #region Event Handlers
- protected override void Startup()
- {
- try
- {
- Core.PluginInitComplete += Core_PluginInitComplete;
- Core.PluginTermComplete += Core_PluginTermComplete;
- Core.FilterInitComplete += Core_FilterInitComplete;
-
- // Set up assembly resolution for hot-loaded plugin dependencies
- AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
-
- // watch the AssemblyDirectory for any .dll file changes
- pluginWatcher = new FileSystemWatcher();
- pluginWatcher.Path = AssemblyDirectory;
- pluginWatcher.NotifyFilter = NotifyFilters.LastWrite;
- pluginWatcher.Filter = "*.dll";
- pluginWatcher.Changed += PluginWatcher_Changed;
- pluginWatcher.EnableRaisingEvents = true;
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void Core_FilterInitComplete(object sender, EventArgs e)
- {
- Core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
- }
-
- private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
- {
- try
- {
- // Login_SendEnterWorldRequest
- if (e.Message.Type == 0xF7C8)
- {
- //EnsurePluginIsDisabledInRegistry();
- }
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void Core_PluginInitComplete(object sender, EventArgs e)
- {
- try
- {
- LoadPluginAssembly();
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void Core_PluginTermComplete(object sender, EventArgs e)
- {
- try
- {
- UnloadPluginAssembly();
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- protected override void Shutdown()
- {
- try
- {
- Core.PluginInitComplete -= Core_PluginInitComplete;
- Core.PluginTermComplete -= Core_PluginTermComplete;
- Core.FilterInitComplete -= Core_FilterInitComplete;
- AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
- UnloadPluginAssembly();
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void Core_RenderFrame(object sender, EventArgs e)
- {
- try
- {
- if (IsPluginLoaded && needsReload && DateTime.UtcNow - LastDllChange > TimeSpan.FromSeconds(1))
- {
- needsReload = false;
- Core.RenderFrame -= Core_RenderFrame;
- isSubscribedToRenderFrame = false;
- LoadPluginAssembly();
- }
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void PluginWatcher_Changed(object sender, FileSystemEventArgs e)
- {
- try
- {
- // Only reload if it's the main plugin DLL
- if (e.Name == PluginAssemblyName)
- {
- LastDllChange = DateTime.UtcNow;
- needsReload = true;
-
- if (!isSubscribedToRenderFrame)
- {
- isSubscribedToRenderFrame = true;
- Core.RenderFrame += Core_RenderFrame;
- }
- }
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
- {
- try
- {
- // Extract just the assembly name (without version info)
- string assemblyName = args.Name.Split(',')[0] + ".dll";
- string assemblyPath = System.IO.Path.Combine(AssemblyDirectory, assemblyName);
-
- // If the dependency exists in our plugin directory, load it
- if (File.Exists(assemblyPath))
- {
- return Assembly.LoadFrom(assemblyPath);
- }
- }
- catch (Exception ex)
- {
- Log($"AssemblyResolve failed for {args.Name}: {ex.Message}");
- }
-
- // Return null to let default resolution continue
- return null;
- }
- #endregion
-
- #region Plugin Loading/Unloading
- internal void LoadPluginAssembly()
- {
- try
- {
- if (IsPluginLoaded)
- {
- UnloadPluginAssembly();
- try
- {
- CoreManager.Current.Actions.AddChatText($"[MosswartMassacre] Reloading {PluginAssemblyName}", 5);
- }
- catch { }
- }
-
- pluginAssembly = Assembly.Load(File.ReadAllBytes(System.IO.Path.Combine(AssemblyDirectory, PluginAssemblyName)));
- pluginType = pluginAssembly.GetType($"{PluginAssemblyNamespace}.PluginCore");
- pluginInstance = Activator.CreateInstance(pluginType);
-
- // Set the AssemblyDirectory property if it exists
- var assemblyDirProperty = pluginType.GetProperty("AssemblyDirectory", BindingFlags.Public | BindingFlags.Static);
- assemblyDirProperty?.SetValue(null, AssemblyDirectory);
-
- // Set the IsHotReload flag if it exists
- var isHotReloadProperty = pluginType.GetProperty("IsHotReload", BindingFlags.Public | BindingFlags.Static);
- isHotReloadProperty?.SetValue(null, true);
-
- // The original template doesn't set up Host - it just calls Startup
- // The plugin should use CoreManager.Current.Actions instead of MyHost for hot reload scenarios
- // We'll set a flag so the plugin knows it's being hot loaded
-
- // Call Startup method
- var startupMethod = pluginType.GetMethod("Startup", BindingFlags.NonPublic | BindingFlags.Instance);
- startupMethod.Invoke(pluginInstance, new object[] { });
-
- IsPluginLoaded = true;
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
-
- private void UnloadPluginAssembly()
- {
- try
- {
- if (pluginInstance != null && pluginType != null)
- {
- MethodInfo shutdownMethod = pluginType.GetMethod("Shutdown", BindingFlags.NonPublic | BindingFlags.Instance);
- shutdownMethod.Invoke(pluginInstance, null);
- pluginInstance = null;
- pluginType = null;
- pluginAssembly = null;
- }
- IsPluginLoaded = false;
- }
- catch (Exception ex)
- {
- Log(ex);
- }
- }
- #endregion
-
- private void Log(Exception ex)
- {
- Log(ex.ToString());
- }
-
- private void Log(string message)
- {
- File.AppendAllText(System.IO.Path.Combine(AssemblyDirectory, "loader_log.txt"), $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n");
- try
- {
- CoreManager.Current.Actions.AddChatText($"[MosswartMassacre.Loader] {message}", 3);
- }
- catch { }
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj b/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
deleted file mode 100644
index 59c8e03..0000000
--- a/MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- net48
- Library
- true
- x86
- 1.0.0
- 8
- {A1B2C3D4-E5F6-7890-1234-567890ABCDEF}
- MosswartMassacre.Loader
- MosswartMassacre.Loader
- true
-
-
- ..\MosswartMassacre\bin\Debug\
- true
- full
-
-
- ..\MosswartMassacre\bin\Release\
- pdbonly
- true
-
-
-
- ..\MosswartMassacre\lib\Decal.Adapter.dll
- False
-
-
- False
- False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL
- False
-
-
-
\ No newline at end of file
diff --git a/MosswartMassacre/CharacterStats.cs b/MosswartMassacre/CharacterStats.cs
deleted file mode 100644
index 5862dc2..0000000
--- a/MosswartMassacre/CharacterStats.cs
+++ /dev/null
@@ -1,376 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Runtime.InteropServices;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using Newtonsoft.Json;
-
-namespace MosswartMassacre
-{
- public struct AllegianceInfoRecord
- {
- public string name;
- public int rank;
- public int race;
- public int gender;
-
- public AllegianceInfoRecord(string _name, int _rank, int _race, int _gender)
- {
- name = _name;
- rank = _rank;
- race = _race;
- gender = _gender;
- }
- }
-
- public static class CharacterStats
- {
- private static IPluginLogger _logger;
-
- // Cached allegiance data (populated from network messages)
- private static string allegianceName;
- private static int allegianceSize;
- private static int followers;
- private static AllegianceInfoRecord monarch;
- private static AllegianceInfoRecord patron;
- private static int allegianceRank;
-
- // Cached luminance data (populated from network messages)
- private static long luminanceEarned = -1;
- private static long luminanceTotal = -1;
-
- // Cached title data (populated from network messages)
- private static int currentTitle = -1;
- private static List titlesList = new List();
-
- // Cached DWORD properties (populated from 0x0013 message)
- private static Dictionary characterProperties = new Dictionary();
-
- // DWORD blacklist — exclude cosmetic/internal properties (same as TreeStats)
- private static readonly HashSet dwordBlacklist = new HashSet {
- 2, 5, 7, 10, 17, 19, 20, 24, 25, 26, 28, 30, 33, 35, 36, 38, 43, 45,
- 86, 87, 88, 89, 90, 91, 92, 98,
- 105, 106, 107, 108, 109, 110, 111, 113, 114, 115, 117, 125, 129, 131, 134,
- 158, 159, 160, 166, 170, 171, 172, 174, 175, 176, 177, 178, 179, 188, 193,
- 270, 271, 272, 293
- };
-
- ///
- /// Reset all cached data. Call on plugin init.
- ///
- internal static void Init(IPluginLogger logger = null)
- {
- _logger = logger;
- allegianceName = null;
- allegianceSize = 0;
- followers = 0;
- monarch = new AllegianceInfoRecord();
- patron = new AllegianceInfoRecord();
- allegianceRank = 0;
- luminanceEarned = -1;
- luminanceTotal = -1;
- currentTitle = -1;
- titlesList.Clear();
- characterProperties.Clear();
- }
-
- ///
- /// Process game event 0x0020 - Allegiance info.
- /// Extracts monarch, patron, rank, followers from the allegiance tree.
- /// Reference: TreeStats Character.cs:642-745
- ///
- internal static void ProcessAllegianceInfoMessage(NetworkMessageEventArgs e)
- {
- try
- {
- allegianceName = e.Message.Value("allegianceName");
- allegianceSize = e.Message.Value("allegianceSize");
- followers = e.Message.Value("followers");
-
- monarch = new AllegianceInfoRecord();
- patron = new AllegianceInfoRecord();
-
- MessageStruct records = e.Message.Struct("records");
- int currentId = CoreManager.Current.CharacterFilter.Id;
- var parentMap = new Dictionary();
- var recordMap = new Dictionary();
-
- for (int i = 0; i < records.Count; i++)
- {
- var record = records.Struct(i);
- int charId = record.Value("character");
- int treeParent = record.Value("treeParent");
-
- parentMap[charId] = treeParent;
- recordMap[charId] = new AllegianceInfoRecord(
- record.Value("name"),
- record.Value("rank"),
- record.Value("race"),
- record.Value("gender"));
-
- // Monarch: treeParent <= 1
- if (treeParent <= 1)
- {
- monarch = recordMap[charId];
- }
- }
-
- // Patron: parent of current character
- if (parentMap.ContainsKey(currentId) && recordMap.ContainsKey(parentMap[currentId]))
- {
- patron = recordMap[parentMap[currentId]];
- }
-
- // Our rank from the record
- if (recordMap.ContainsKey(currentId))
- {
- allegianceRank = recordMap[currentId].rank;
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Allegiance processing error: {ex.Message}");
- }
- }
-
- ///
- /// Process game event 0x0013 - Character property data.
- /// Extracts luminance from QWORD keys 6 and 7.
- /// Reference: TreeStats Character.cs:582-640
- ///
- internal static void ProcessCharacterPropertyData(NetworkMessageEventArgs e)
- {
- try
- {
- MessageStruct props = e.Message.Struct("properties");
- MessageStruct qwords = props.Struct("qwords");
-
- for (int i = 0; i < qwords.Count; i++)
- {
- var tmpStruct = qwords.Struct(i);
- long key = tmpStruct.Value("key");
- long value = tmpStruct.Value("value");
-
- if (key == Constants.AvailableLuminanceKey)
- luminanceEarned = value;
- else if (key == Constants.MaximumLuminanceKey)
- luminanceTotal = value;
- }
-
- // Parse DWORD properties (augmentations, ratings, masteries, society, etc.)
- MessageStruct dwords = props.Struct("dwords");
- characterProperties.Clear();
- for (int i = 0; i < dwords.Count; i++)
- {
- var tmpStruct = dwords.Struct(i);
- int key = tmpStruct.Value("key");
- int value = tmpStruct.Value("value");
- if (!dwordBlacklist.Contains(key))
- characterProperties[key] = value;
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Property processing error: {ex.Message}");
- }
- }
-
- ///
- /// Process message 0x02CF - PrivateUpdatePropertyInt64.
- /// Sent during gameplay when an Int64 property changes (e.g., luminance earned/spent).
- /// Wire format after 4-byte type header: sequence(1) + key(4) + value(8).
- /// Uses RawData since Decal's messages.xml may not define this message type.
- ///
- internal static void ProcessPropertyInt64Update(NetworkMessageEventArgs e)
- {
- try
- {
- byte[] raw = e.Message.RawData;
- if (raw.Length < 17) return; // 4 type + 1 seq + 4 key + 8 value
-
- int key = BitConverter.ToInt32(raw, 5);
- long value = BitConverter.ToInt64(raw, 9);
-
- if (key == Constants.AvailableLuminanceKey)
- luminanceEarned = value;
- else if (key == Constants.MaximumLuminanceKey)
- luminanceTotal = value;
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Int64 property update error: {ex.Message}");
- }
- }
-
- ///
- /// Process game event 0x0029 - Titles list.
- /// Extracts current title ID.
- /// Reference: TreeStats Character.cs:551-580
- ///
- internal static void ProcessTitlesMessage(NetworkMessageEventArgs e)
- {
- try
- {
- // Capture full titles list
- MessageStruct titles = e.Message.Struct("titles");
- titlesList.Clear();
- for (int i = 0; i < titles.Count; i++)
- {
- titlesList.Add(titles.Struct(i).Value("value"));
- }
-
- currentTitle = e.Message.Value("current");
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Title processing error: {ex.Message}");
- }
- }
-
- ///
- /// Process game event 0x002b - Set title (when player changes title).
- ///
- internal static void ProcessSetTitleMessage(NetworkMessageEventArgs e)
- {
- try
- {
- currentTitle = e.Message.Value("title");
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Set title error: {ex.Message}");
- }
- }
-
- ///
- /// Collect all character data and send via WebSocket.
- /// Called on login (after delay) and every 10 minutes.
- ///
- internal static void CollectAndSend()
- {
- if (!PluginCore.WebSocketEnabled)
- return;
-
- try
- {
- var cf = CoreManager.Current.CharacterFilter;
- var culture = new CultureInfo("en-US");
-
- // --- Attributes ---
- var attributes = new Dictionary();
- foreach (var attr in cf.Attributes)
- {
- attributes[attr.Name.ToLower()] = new
- {
- @base = attr.Base,
- creation = attr.Creation
- };
- }
-
- // --- Vitals (base values) ---
- var vitals = new Dictionary();
- foreach (var vital in cf.Vitals)
- {
- vitals[vital.Name.ToLower()] = new
- {
- @base = vital.Base
- };
- }
-
- // --- Skills ---
- var skills = new Dictionary();
- Decal.Filters.FileService fs = CoreManager.Current.FileService as Decal.Filters.FileService;
- if (fs != null)
- {
- for (int i = 0; i < fs.SkillTable.Length; i++)
- {
- Decal.Interop.Filters.SkillInfo skillinfo = null;
- try
- {
- skillinfo = cf.Underlying.get_Skill(
- (Decal.Interop.Filters.eSkillID)fs.SkillTable[i].Id);
-
- string name = skillinfo.Name.ToLower().Replace(" ", "_");
- string training = skillinfo.Training.ToString();
- // Training enum returns "eTrainSpecialized" etc, strip "eTrain" prefix
- if (training.Length > 6)
- training = training.Substring(6);
-
- skills[name] = new
- {
- @base = skillinfo.Base,
- training = training
- };
- }
- finally
- {
- if (skillinfo != null)
- {
- Marshal.ReleaseComObject(skillinfo);
- skillinfo = null;
- }
- }
- }
- }
-
- // --- Allegiance ---
- object allegiance = null;
- if (allegianceName != null)
- {
- allegiance = new
- {
- name = allegianceName,
- monarch = monarch.name != null ? new
- {
- name = monarch.name,
- race = monarch.race,
- rank = monarch.rank,
- gender = monarch.gender
- } : null,
- patron = patron.name != null ? new
- {
- name = patron.name,
- race = patron.race,
- rank = patron.rank,
- gender = patron.gender
- } : null,
- rank = allegianceRank,
- followers = followers
- };
- }
-
- // --- Build payload ---
- var payload = new
- {
- type = "character_stats",
- timestamp = DateTime.UtcNow.ToString("o"),
- character_name = cf.Name,
- level = cf.Level,
- race = cf.Race,
- gender = cf.Gender,
- birth = cf.Birth.ToString(culture),
- total_xp = cf.TotalXP,
- unassigned_xp = cf.UnassignedXP,
- skill_credits = cf.SkillPoints,
- deaths = cf.Deaths,
- luminance_earned = luminanceEarned >= 0 ? (long?)luminanceEarned : null,
- luminance_total = luminanceTotal >= 0 ? (long?)luminanceTotal : null,
- current_title = currentTitle >= 0 ? (int?)currentTitle : null,
- attributes = attributes,
- vitals = vitals,
- skills = skills,
- allegiance = allegiance,
- properties = characterProperties.Count > 0 ? new Dictionary(characterProperties) : null,
- titles = titlesList.Count > 0 ? new List(titlesList) : null
- };
-
- _ = WebSocket.SendCharacterStatsAsync(payload);
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] Error collecting stats: {ex.Message}");
- }
- }
- }
-}
diff --git a/MosswartMassacre/ChatEventRouter.cs b/MosswartMassacre/ChatEventRouter.cs
deleted file mode 100644
index 9729ffa..0000000
--- a/MosswartMassacre/ChatEventRouter.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Text.RegularExpressions;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-
-namespace MosswartMassacre
-{
- ///
- /// Routes chat events to the appropriate handler (KillTracker, RareTracker, etc.)
- /// Replaces the big if/else chain in PluginCore.OnChatText.
- ///
- internal class ChatEventRouter
- {
- private readonly IPluginLogger _logger;
- private readonly KillTracker _killTracker;
- private RareTracker _rareTracker;
- private readonly Action _onRareCountChanged;
- private readonly Action _onAllegianceReport;
-
- internal void SetRareTracker(RareTracker rareTracker) => _rareTracker = rareTracker;
-
- internal ChatEventRouter(
- IPluginLogger logger,
- KillTracker killTracker,
- RareTracker rareTracker,
- Action onRareCountChanged,
- Action onAllegianceReport)
- {
- _logger = logger;
- _killTracker = killTracker;
- _rareTracker = rareTracker;
- _onRareCountChanged = onRareCountChanged;
- _onAllegianceReport = onAllegianceReport;
- }
-
- internal void OnChatText(object sender, ChatTextInterceptEventArgs e)
- {
- try
- {
- _killTracker.CheckForKill(e.Text);
-
- if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
- {
- _killTracker.RareCount = _rareTracker.RareCount;
- _onRareCountChanged?.Invoke(_rareTracker.RareCount);
- }
-
- if (e.Color == 18 && e.Text.EndsWith("!report\""))
- {
- TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
- string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}";
- _logger?.Log($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
- _onAllegianceReport?.Invoke(reportMessage);
- }
- }
- catch (Exception ex)
- {
- _logger?.Log("Error processing chat message: " + ex.Message);
- }
- }
-
- ///
- /// Streams all chat text to WebSocket (separate handler from the filtered one above).
- ///
- internal static async void AllChatText(object sender, ChatTextInterceptEventArgs e)
- {
- try
- {
- string cleaned = NormalizeChatLine(e.Text);
- await WebSocket.SendChatTextAsync(e.Color, cleaned);
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
- }
- }
-
- private static string NormalizeChatLine(string raw)
- {
- if (string.IsNullOrEmpty(raw))
- return raw;
-
- var noTags = Regex.Replace(raw, "<[^>]+>", "");
- var trimmed = noTags.TrimEnd('\r', '\n');
- var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
-
- return collapsed;
- }
- }
-}
diff --git a/MosswartMassacre/ChestLooter.cs b/MosswartMassacre/ChestLooter.cs
deleted file mode 100644
index a91c619..0000000
--- a/MosswartMassacre/ChestLooter.cs
+++ /dev/null
@@ -1,1346 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using Decal.Interop.Input;
-using Mag.Shared.Constants;
-using Timer = Decal.Interop.Input.Timer;
-
-namespace MosswartMassacre
-{
- ///
- /// Chest Looter - Automated chest looting with VTank loot profile integration
- /// Ported from Utility Belt with enhancements for name-based chest/key selection
- ///
- public class ChestLooter : IDisposable
- {
- #region Enums
-
- private enum ItemState
- {
- None = 0x0001,
- InContainer = 0x0002,
- RequestingInfo = 0x0004,
- NeedsToBeLooted = 0x0008,
- Ignore = 0x0016,
- Looted = 0x0032,
- Blacklisted = 0x0064
- }
-
- private enum LooterState
- {
- Closed = 0x0001,
- Unlocking = 0x0002,
- Locked = 0x0004,
- Unlocked = 0x0008,
- Opening = 0x00016,
- Open = 0x0032,
- Looting = 0x0064,
- Closing = 0x0128,
- Salvaging = 0x0256,
- Done = 0x0512
- }
-
- public enum RunType
- {
- None = 0x0001,
- CommandLine = 0x0002, // Started via /mm commands
- UI = 0x0004 // Started via UI button
- }
-
- private enum ContainerType
- {
- Chest = 0x0001,
- MyCorpse = 0x0002,
- MonsterCorpse = 0x0004,
- Unknown = 0x0008
- }
-
- #endregion
-
- #region Fields
-
- private readonly CoreManager core;
- private readonly ChestLooterSettings settings;
-
- private Dictionary containerItems = new Dictionary();
- private List containerItemsList = new List();
-
- private bool dispatchEnabled = false;
- private bool ubOwnedContainer = false;
- private bool first = true;
-
- private int targetContainerID = 0;
- private int targetKeyID = 0;
- private string targetKeyName = "";
- private string targetChestName = "";
-
- private int lootAttemptCount = 0;
- private int unlockAttempt = 0;
- private int openAttempt = 0;
- private int lastOpenContainer = 0;
-
- private LooterState looterState = LooterState.Done;
- private LooterState lastLooterState = LooterState.Done;
- private RunType runType = RunType.None;
- private ContainerType containerType = ContainerType.Unknown;
-
- private DateTime startTime = DateTime.MinValue;
- private DateTime lastAttempt = DateTime.UtcNow;
-
- private TimerClass baseTimer;
- private bool disposed = false;
-
- #endregion
-
- #region Events
-
- public event EventHandler LooterStarted;
- public event EventHandler LooterFinished;
- public event EventHandler LooterFinishedForceStop;
- public event EventHandler StatusChanged;
-
- #endregion
-
- #region Properties
-
- public bool IsRunning => looterState != LooterState.Done;
- public RunType CurrentRunType => runType;
- public string CurrentStatus => GetStatusString();
-
- #endregion
-
- #region Constructor & Initialization
-
- public ChestLooter(CoreManager coreManager, ChestLooterSettings chestLooterSettings)
- {
- core = coreManager ?? throw new ArgumentNullException(nameof(coreManager));
- settings = chestLooterSettings ?? throw new ArgumentNullException(nameof(chestLooterSettings));
-
- settings.Validate();
- }
-
- public void Initialize()
- {
- try
- {
- if (settings.Enabled)
- {
- EnableDispatch();
- }
- }
- catch (Exception ex)
- {
- LogError($"ChestLooter initialization error: {ex.Message}");
- }
- }
-
- #endregion
-
- #region Public Methods
-
- ///
- /// Start looting using configured chest and key names
- ///
- public bool StartByName()
- {
- return StartByName(settings.ChestName, settings.KeyName);
- }
-
- ///
- /// Start looting with specified chest and key names
- ///
- public bool StartByName(string chestName, string keyName)
- {
- try
- {
- if (IsRunning)
- {
- LogError("Looter is already running. Stop it first.");
- return false;
- }
-
- if (string.IsNullOrEmpty(chestName))
- {
- LogError("Chest name not set. Use /mm setchest ");
- return false;
- }
-
- if (string.IsNullOrEmpty(keyName))
- {
- LogError("Key name not set. Use /mm setkey ");
- return false;
- }
-
- // Find the closest chest by name
- var chest = Utils.FindClosestChestByName(chestName);
- if (chest == null)
- {
- LogError($"Could not find chest named '{chestName}'");
- return false;
- }
-
- // Find the key in inventory
- var key = Utils.FindKeyInInventory(keyName);
- if (key == null)
- {
- LogError($"Could not find key named '{keyName}' in inventory");
- return false;
- }
-
- targetChestName = chestName;
- targetKeyName = keyName;
- targetContainerID = chest.Id;
- targetKeyID = key.Id;
-
- Log($"Starting chest looter: chest='{chest.Name}' key='{key.Name}'");
-
- return StartInternal(RunType.CommandLine);
- }
- catch (Exception ex)
- {
- LogError($"Error starting looter by name: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// Start looting with manually selected chest and key
- ///
- public bool StartWithSelection(int chestId, string keyName)
- {
- try
- {
- if (IsRunning)
- {
- LogError("Looter is already running. Stop it first.");
- return false;
- }
-
- if (!core.Actions.IsValidObject(chestId))
- {
- LogError("Invalid chest selection");
- return false;
- }
-
- var key = Utils.FindKeyInInventory(keyName);
- if (key == null)
- {
- LogError($"Could not find key named '{keyName}' in inventory");
- return false;
- }
-
- targetContainerID = chestId;
- targetKeyID = key.Id;
- targetKeyName = keyName;
-
- return StartInternal(RunType.UI);
- }
- catch (Exception ex)
- {
- LogError($"Error starting looter with selection: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// Stop the looter
- ///
- public void Stop()
- {
- try
- {
- if (!IsRunning)
- return;
-
- Log("Stopping looter...");
- DoneLooting(false, true);
- }
- catch (Exception ex)
- {
- LogError($"Error stopping looter: {ex.Message}");
- }
- }
-
- ///
- /// Set the chest name for next run
- ///
- public void SetChestName(string name)
- {
- settings.ChestName = name;
- Log($"Chest name set to: {name}");
- }
-
- ///
- /// Set the key name for next run
- ///
- public void SetKeyName(string name)
- {
- settings.KeyName = name;
- Log($"Key name set to: {name}");
- }
-
- #endregion
-
- #region Private Core Methods
-
- private bool StartInternal(RunType type)
- {
- try
- {
- looterState = LooterState.Unlocking;
- lastAttempt = DateTime.UtcNow;
- runType = type;
- ubOwnedContainer = true;
- startTime = DateTime.UtcNow;
-
- EnableDispatch();
- EnableBaseTimer();
- LockVtankSettings(); // Lock VTank to prevent interference
-
- LooterStarted?.Invoke(this, EventArgs.Empty);
- UpdateStatus("Starting looter...");
-
- return true;
- }
- catch (Exception ex)
- {
- LogError($"Error in StartInternal: {ex.Message}");
- return false;
- }
- }
-
- private void EnableDispatch()
- {
- try
- {
- if (!dispatchEnabled)
- {
- core.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch;
- core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
- dispatchEnabled = true;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error enabling dispatch: {ex.Message}");
- }
- }
-
- private void DisableDispatch()
- {
- try
- {
- if (dispatchEnabled && !settings.Enabled)
- {
- core.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch;
- core.EchoFilter.ClientDispatch -= EchoFilter_ClientDispatch;
- dispatchEnabled = false;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error disabling dispatch: {ex.Message}");
- }
- }
-
- private void EnableBaseTimer()
- {
- try
- {
- if (baseTimer == null)
- {
- baseTimer = new TimerClass();
- baseTimer.Timeout += BaseTimer_Timeout;
- baseTimer.Start(settings.OverallSpeed);
- }
- }
- catch (Exception ex)
- {
- LogError($"Error enabling base timer: {ex.Message}");
- }
- }
-
- private void DisableBaseTimer()
- {
- try
- {
- if (baseTimer != null)
- {
- baseTimer.Timeout -= BaseTimer_Timeout;
- baseTimer.Stop();
- baseTimer = null;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error disabling base timer: {ex.Message}");
- }
- }
-
- private void DoneLooting(bool writeChat = true, bool force = false)
- {
- try
- {
- var itemsInContainer = containerItems.Where(x => x.Value == ItemState.InContainer);
- var needsToBeLootedItems = containerItems.Where(x => x.Value == ItemState.NeedsToBeLooted);
- var lootedItems = containerItems.Where(x => x.Value == ItemState.Looted);
- var blacklistedItems = containerItems.Where(x => x.Value == ItemState.Blacklisted);
- var ignoredItems = containerItems.Where(x => x.Value == ItemState.Ignore);
- var requestingInfoItems = containerItems.Where(x => x.Value == ItemState.RequestingInfo);
-
- if (writeChat)
- {
- var elapsed = DateTime.UtcNow - startTime;
- var testMode = settings.TestMode ? " [TEST MODE] " : "";
-
- if (needsToBeLootedItems.Count() + blacklistedItems.Count() <= 0)
- {
- Log($"Looter{testMode} completed in {elapsed.TotalSeconds:F1}s - Scanned {containerItems.Count} items, looted {lootedItems.Count()} items");
- }
- else
- {
- Log($"Looter{testMode} completed in {elapsed.TotalSeconds:F1}s - Scanned {containerItems.Count} items, looted {lootedItems.Count()} items - Failed to loot {needsToBeLootedItems.Count() + blacklistedItems.Count()} items");
- }
- }
-
- // Unlock VTank FIRST (like UB does)
- UnlockVtankSettings();
-
- // Reset state (matching UB exactly)
- containerItems.Clear();
- containerItemsList.Clear();
- looterState = LooterState.Done;
- lastLooterState = LooterState.Done;
- ubOwnedContainer = false;
- lootAttemptCount = 0;
- unlockAttempt = 0;
- openAttempt = 0;
- targetKeyID = 0;
- containerType = ContainerType.Unknown;
- startTime = DateTime.MinValue;
- lastAttempt = DateTime.UtcNow;
-
- // FIXED: Check force FIRST - skip restart entirely if force stopping
- if (force)
- {
- // Force stop - don't restart, just cleanup
- LooterFinishedForceStop?.Invoke(this, EventArgs.Empty);
- targetKeyName = "";
- targetChestName = "";
- targetContainerID = 0;
- runType = RunType.None;
- DisableBaseTimer();
- if (!settings.Enabled && dispatchEnabled)
- {
- DisableDispatch();
- }
- UpdateStatus("Ready");
- return; // Exit early - don't check for more keys
- }
-
- // Not force - check if we should restart with more keys
- if ((runType == RunType.UI || runType == RunType.CommandLine) && HasMoreKeys(targetKeyName))
- {
- // Restart the whole process like UB does with StartUI
- StartUI(targetContainerID, targetKeyName);
- }
- else
- {
- // No more keys - cleanup
- targetKeyName = "";
- targetChestName = "";
- targetContainerID = 0;
- runType = RunType.None;
- DisableBaseTimer();
- if (!settings.Enabled && dispatchEnabled)
- {
- DisableDispatch();
- }
- UpdateStatus("Ready");
- LooterFinished?.Invoke(this, EventArgs.Empty);
- }
- }
- catch (Exception ex)
- {
- LogError($"Error in DoneLooting: {ex.Message}");
- }
- }
-
- ///
- /// Start looting via UI/CommandLine - matches UB's StartUI method
- ///
- private void StartUI(int container, string key)
- {
- try
- {
- targetContainerID = container;
- targetKeyName = key;
-
- // Find key by EXACT name match (like UB)
- var keyObj = FindKeyByExactName(key);
- if (keyObj == null)
- {
- Log("Looter: Out of keys to use");
- LooterFinished?.Invoke(this, EventArgs.Empty);
- DoneLooting(false, true);
- return;
- }
-
- targetKeyID = keyObj.Id;
-
- looterState = LooterState.Unlocking;
- lastAttempt = DateTime.UtcNow;
- // Keep runType as is (UI or CommandLine)
- ubOwnedContainer = true;
- first = true;
- unlockAttempt = 0;
- openAttempt = 0;
-
- EnableDispatch();
- EnableBaseTimer();
- }
- catch (Exception ex)
- {
- LogError($"Error in StartUI: {ex.Message}");
- }
- }
-
- ///
- /// Find a key by EXACT name match (like UB's Util.FindInventoryObjectByName)
- ///
- private WorldObject FindKeyByExactName(string name)
- {
- try
- {
- using (var inv = core.WorldFilter.GetInventory())
- {
- foreach (var wo in inv)
- {
- if (wo.Name == name)
- {
- return wo;
- }
- }
- }
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- private void StopAndCleanup()
- {
- targetKeyName = "";
- targetChestName = "";
- targetContainerID = 0;
- runType = RunType.None;
-
- DisableBaseTimer();
- DisableDispatch();
-
- LooterFinished?.Invoke(this, EventArgs.Empty);
- UpdateStatus("Ready");
- }
-
- ///
- /// Lock VTank to prevent it from interfering with looting (matches UB's LockVtankSettings)
- ///
- private void LockVtankSettings()
- {
- try
- {
- if (vTank.Instance == null) return;
-
- // Lock navigation to prevent moving away from chest
- vTank.Decision_Lock(uTank2.ActionLockType.Navigation, TimeSpan.FromMilliseconds(30000));
- // Lock void spells (debuffs)
- vTank.Decision_Lock(uTank2.ActionLockType.VoidSpellLockedOut, TimeSpan.FromMilliseconds(30000));
- // Lock war spells (attacks)
- vTank.Decision_Lock(uTank2.ActionLockType.WarSpellLockedOut, TimeSpan.FromMilliseconds(30000));
- // Lock melee attacks
- vTank.Decision_Lock(uTank2.ActionLockType.MeleeAttackShot, TimeSpan.FromMilliseconds(30000));
- // Lock item use to prevent VTank from using items
- vTank.Decision_Lock(uTank2.ActionLockType.ItemUse, TimeSpan.FromMilliseconds(30000));
- }
- catch (Exception ex)
- {
- LogError($"Error locking VTank: {ex.Message}");
- }
- }
-
- ///
- /// Unlock VTank after looting is complete (matches UB's UnLockVtankSettings)
- ///
- private void UnlockVtankSettings()
- {
- try
- {
- if (vTank.Instance == null) return;
-
- vTank.Decision_UnLock(uTank2.ActionLockType.Navigation);
- vTank.Decision_UnLock(uTank2.ActionLockType.VoidSpellLockedOut);
- vTank.Decision_UnLock(uTank2.ActionLockType.WarSpellLockedOut);
- vTank.Decision_UnLock(uTank2.ActionLockType.MeleeAttackShot);
- vTank.Decision_UnLock(uTank2.ActionLockType.ItemUse);
- }
- catch (Exception ex)
- {
- LogError($"Error unlocking VTank: {ex.Message}");
- }
- }
-
- #endregion
-
- #region Network Event Handlers
-
- private void EchoFilter_ServerDispatch(object sender, NetworkMessageEventArgs e)
- {
- try
- {
- switch (e.Message.Type)
- {
- case 0xF7B0:
- switch (e.Message.Value("event"))
- {
- case 0x0196: // Item_OnViewContents - chest opened
- HandleChestOpened(e);
- break;
-
- case 0x01C7: // Item_UseDone (Failure Type)
- if (e.Message.Value("unknown") == 1201) // Already open
- {
- if (core.Actions.OpenedContainer == targetContainerID)
- {
- looterState = LooterState.Open;
- }
- }
- break;
-
- case 0x0022: // Item_ServerSaysContainID (moved object)
- int movedObjectId = e.Message.Value("item");
- int movedContainerId = e.Message.Value("container");
- if (containerItems.ContainsKey(movedObjectId) && IsContainerPlayer(movedContainerId))
- {
- LootedItem(movedObjectId);
- }
- break;
- }
- break;
-
- case 0x0024: // Object deleted (stacked item looted)
- int removedObjectId = e.Message.Value("object");
- LootedItem(removedObjectId);
- break;
-
- case 0xF750: // Chest unlocked/locked
- HandleChestLockState(e);
- break;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error in ServerDispatch: {ex.Message}");
- }
- }
-
- private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
- {
- try
- {
- switch (e.Message.Type)
- {
- case 0xF7B1:
- switch (e.Message.Value("action"))
- {
- case 0x0019: // Item moved into container
- int itemIntoContainer = e.Message.Value("item");
- if (containerItems.ContainsKey(itemIntoContainer))
- {
- lootAttemptCount++;
- }
- break;
- }
- break;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error in ClientDispatch: {ex.Message}");
- }
- }
-
- private void HandleChestOpened(NetworkMessageEventArgs e)
- {
- try
- {
- // Match UB's condition exactly: if (Enabled || runType == runtype.UI)
- // For us: settings.Enabled OR runType is UI/CommandLine
- if (settings.Enabled || runType == RunType.UI || runType == RunType.CommandLine)
- {
- int containerID = e.Message.Value("container");
- var itemCount = e.Message.Value("itemCount");
- var items = e.Message.Struct("items");
-
- if (itemCount <= 0) return;
- if (containerID == 0) return;
- if (!IsValidContainer(containerID)) return;
-
- containerType = GetContainerType(containerID);
-
- // Return if container isn't enabled in settings
- if (containerType == ContainerType.Unknown) return;
- // EnableChests check only when NOT running via UI/CommandLine
- if (containerType == ContainerType.Chest && !settings.EnableChests && runType != RunType.UI && runType != RunType.CommandLine) return;
-
- // Past returns, now we do things
- startTime = DateTime.UtcNow;
- targetContainerID = containerID;
-
- // When we receive chest contents, the chest is open - transition to Open state
- // UB relies on HandleStateOpening checking OpenedContainer, but we can be more direct
- if (looterState != LooterState.Looting)
- {
- looterState = LooterState.Open;
- }
-
- // Add all items in chest to tracking
- for (int i = 0; i < itemCount; i++)
- {
- int woid = items.Struct(i).Value("item");
- ItemState state = ItemState.InContainer;
- UpdateContainerItems(woid, state);
- }
-
- Log($"HandleChestOpened: Added {itemCount} items, containerItems.Count={containerItems.Count}, state={looterState}");
-
- EnableBaseTimer();
- UpdateStatus($"Chest opened, scanning {itemCount} items...");
- }
- else
- {
- // UB's else branch
- looterState = LooterState.Open;
- DoneLooting(false, false);
- }
- }
- catch (Exception ex)
- {
- LogError($"Error handling chest opened: {ex.Message}");
- }
- }
-
- private void HandleChestLockState(NetworkMessageEventArgs e)
- {
- try
- {
- int lockedObjID = e.Message.Value("object");
- int lockedObjIDEffect = e.Message.Value("effect");
-
- if (ubOwnedContainer && lockedObjID == targetContainerID)
- {
- if (lockedObjIDEffect == 148) // Locked
- {
- if (looterState == LooterState.Closing)
- {
- looterState = LooterState.Closed;
- }
- else
- {
- looterState = LooterState.Locked;
- }
- }
- else if (lockedObjIDEffect == 147) // Unlocked
- {
- looterState = LooterState.Unlocked;
- UpdateStatus("Chest unlocked");
- }
- }
- }
- catch (Exception ex)
- {
- LogError($"Error handling chest lock state: {ex.Message}");
- }
- }
-
- #endregion
-
- #region Timer Handler
-
- private void BaseTimer_Timeout(Timer Source)
- {
- try
- {
- if (lastOpenContainer != core.Actions.OpenedContainer)
- {
- lastOpenContainer = core.Actions.OpenedContainer;
- }
-
- if (lastLooterState != looterState)
- {
- first = true;
- lastLooterState = looterState;
- }
-
- switch (looterState)
- {
- case LooterState.Done:
- // Nothing to do - timer will be stopped when appropriate
- break;
- case LooterState.Closed:
- HandleStateClosed();
- break;
- case LooterState.Locked:
- HandleStateLocked();
- break;
- case LooterState.Unlocked:
- HandleStateUnlocked();
- break;
- case LooterState.Unlocking:
- HandleStateUnlocking();
- break;
- case LooterState.Open:
- HandleStateOpen();
- break;
- case LooterState.Opening:
- HandleStateOpening();
- break;
- case LooterState.Looting:
- HandleStateLooting();
- break;
- case LooterState.Closing:
- HandleStateClosing();
- break;
- }
- }
- catch (Exception ex)
- {
- LogError($"Error in BaseTimer_Timeout: {ex.Message}");
- }
- }
-
- #endregion
-
- #region State Handlers
-
- private void HandleStateClosed()
- {
- if (ubOwnedContainer)
- {
- looterState = LooterState.Done;
- DoneLooting();
- }
- else
- {
- looterState = LooterState.Done;
- }
- }
-
- private void HandleStateLocked()
- {
- if (runType == RunType.UI || runType == RunType.CommandLine)
- {
- looterState = LooterState.Unlocking;
- }
- else
- {
- DoneLooting(false, false);
- }
- }
-
- private void HandleStateUnlocked()
- {
- looterState = LooterState.Opening;
- lastAttempt = DateTime.UtcNow;
- }
-
- private void HandleStateUnlocking()
- {
- if (first || DateTime.UtcNow - lastAttempt >= TimeSpan.FromMilliseconds(settings.DelaySpeed))
- {
- if (unlockAttempt >= settings.MaxUnlockAttempts)
- {
- DoneLooting(false, false);
- return;
- }
-
- lastAttempt = DateTime.UtcNow;
- core.Actions.SelectItem(targetKeyID);
- core.Actions.ApplyItem(targetKeyID, targetContainerID);
- unlockAttempt++;
- first = false;
- UpdateStatus($"Unlocking... (attempt {unlockAttempt}/{settings.MaxUnlockAttempts})");
- }
- }
-
- private void HandleStateOpen()
- {
- looterState = LooterState.Looting;
- UpdateStatus("Looting items...");
- }
-
- private void HandleStateOpening()
- {
- if (core.Actions.OpenedContainer == targetContainerID)
- {
- looterState = LooterState.Open;
- return;
- }
-
- if (first || DateTime.UtcNow - lastAttempt >= TimeSpan.FromMilliseconds(settings.DelaySpeed))
- {
- if (openAttempt >= settings.MaxOpenAttempts)
- {
- Log("Reached max open attempts");
- DoneLooting(false, false);
- return;
- }
-
- lastAttempt = DateTime.UtcNow;
- core.Actions.UseItem(targetContainerID, 0);
- openAttempt++;
- first = false;
- UpdateStatus($"Opening... (attempt {openAttempt}/{settings.MaxOpenAttempts})");
- }
- }
-
- private void HandleStateLooting()
- {
- var itemsInContainer = containerItems.Where(x => x.Value == ItemState.InContainer);
- var needsToBeLootedItems = containerItems.Where(x => x.Value == ItemState.NeedsToBeLooted);
- var lootedItems = containerItems.Where(x => x.Value == ItemState.Looted);
- var blacklistedItems = containerItems.Where(x => x.Value == ItemState.Blacklisted);
- var ignoredItems = containerItems.Where(x => x.Value == ItemState.Ignore);
- var requestingInfoItems = containerItems.Where(x => x.Value == ItemState.RequestingInfo);
-
- // Process items that need classification (InContainer first, then RequestingInfo)
- if (itemsInContainer.Count() > 0)
- {
- GetLootDecision(itemsInContainer.First().Key);
- }
- else if (requestingInfoItems.Count() > 0)
- {
- // Re-check items that were waiting for ID data
- GetLootDecision(requestingInfoItems.First().Key);
- }
-
- // Loot items
- foreach (int item in needsToBeLootedItems.Select(w => w.Key).ToList())
- {
- if (!settings.TestMode)
- {
- core.Actions.MoveItem(item, core.CharacterFilter.Id);
- if (needsToBeLootedItems.First().Key == item)
- {
- lootAttemptCount++;
- }
- }
- else
- {
- containerItems[item] = ItemState.Looted;
- }
-
- if (lootAttemptCount >= settings.AttemptsBeforeBlacklisting)
- {
- int blackListedItem = needsToBeLootedItems.First().Key;
- containerItems[blackListedItem] = ItemState.Blacklisted;
- }
- }
-
- // Check if done looting (all items are either looted, blacklisted, or ignored)
- if (containerItems.Count() > 0 &&
- lootedItems.Count() + blacklistedItems.Count() + ignoredItems.Count() == containerItemsList.Count())
- {
- if (ubOwnedContainer)
- {
- lastAttempt = DateTime.UtcNow;
- looterState = LooterState.Closing;
- }
- else
- {
- DoneLooting();
- }
- }
-
- UpdateStatus($"Looting... ({lootedItems.Count()}/{containerItems.Count} items)");
- }
-
- private void HandleStateClosing()
- {
- // Check if container is closed (match UB exactly - only check OpenedContainer == 0)
- if (core.Actions.OpenedContainer == 0)
- {
- looterState = LooterState.Closed;
- return;
- }
-
- // Try to close the chest
- if (first || DateTime.UtcNow - lastAttempt >= TimeSpan.FromMilliseconds(settings.DelaySpeed))
- {
- lastAttempt = DateTime.UtcNow;
- core.Actions.UseItem(targetContainerID, 0);
- first = false;
- UpdateStatus("Closing chest...");
- }
- }
-
- #endregion
-
- #region Loot Decision Logic
-
- ///
- /// Determine if an item should be looted based on VTank loot profile
- /// Ported directly from Utility Belt's Looter.cs GetLootDecision method
- ///
- private void GetLootDecision(int item)
- {
- try
- {
- if (!core.Actions.IsValidObject(item))
- {
- containerItems.Remove(item);
- containerItemsList.Remove(item);
- return;
- }
-
- var wo = core.WorldFilter[item];
-
- // Check if item needs ID first (red rules in VTank profile)
- bool needsId = false;
- try
- {
- needsId = uTank2.PluginCore.PC.FLootPluginQueryNeedsID(item);
- }
- catch
- {
- containerItems[item] = ItemState.Ignore;
- return;
- }
-
- if (!needsId)
- {
- // Item doesn't need ID, classify immediately
- dynamic result = null;
- try
- {
- result = uTank2.PluginCore.PC.FLootPluginClassifyImmediate(item);
- }
- catch
- {
- containerItems[item] = ItemState.Ignore;
- return;
- }
-
- if (result.IsKeep)
- {
- containerItems[item] = ItemState.NeedsToBeLooted;
- }
-
- if (result.IsKeepUpTo)
- {
- // IsKeepUpTo - check inventory count for this rule
- int itemCount = 0;
- bool waitingForInvItems = false;
-
- using (var inv = core.WorldFilter.GetInventory())
- {
- foreach (WorldObject invItem in inv)
- {
- waitingForInvItems = false;
-
- if (uTank2.PluginCore.PC.FLootPluginQueryNeedsID(invItem.Id))
- {
- core.Actions.RequestId(invItem.Id);
- waitingForInvItems = true;
- break;
- }
- else
- {
- var invItemResult = uTank2.PluginCore.PC.FLootPluginClassifyImmediate(invItem.Id);
- if (result.RuleName == invItemResult.RuleName)
- {
- if (invItem.Values(LongValueKey.StackMax, 0) > 0)
- {
- itemCount += invItem.Values(LongValueKey.StackCount, 1);
- }
- else
- {
- itemCount++;
- }
- }
- }
- }
- }
-
- if (!waitingForInvItems)
- {
- if (itemCount >= result.Data1)
- {
- containerItems[item] = ItemState.Ignore;
- }
- else
- {
- containerItems[item] = ItemState.NeedsToBeLooted;
- }
- }
- }
-
- if (result.IsSalvage)
- {
- // Treat salvage items as loot (salvage feature not implemented)
- containerItems[item] = ItemState.NeedsToBeLooted;
- }
-
- if (result.IsNoLoot)
- {
- containerItems[item] = ItemState.Ignore;
- }
-
- // If none of the above matched, item stays in current state (InContainer)
- // This means it doesn't match any rule - we ignore it
- if (!result.IsKeep && !result.IsKeepUpTo && !result.IsSalvage && !result.IsNoLoot)
- {
- containerItems[item] = ItemState.Ignore;
- }
-
- if (settings.TestMode)
- {
- containerItems[item] = ItemState.Looted;
- }
- }
- else
- {
- // Item needs ID - request it and mark as RequestingInfo
- if (containerItems[item] != ItemState.RequestingInfo)
- {
- core.Actions.RequestId(item);
- containerItems[item] = ItemState.RequestingInfo;
- }
- }
- }
- catch
- {
- containerItems[item] = ItemState.Ignore;
- }
- }
-
- #endregion
-
- #region Helper Methods
-
- private bool IsValidContainer(int containerId)
- {
- try
- {
- WorldObject containerWO = core.WorldFilter[containerId];
- if (containerWO.ObjectClass != ObjectClass.Container &&
- containerWO.ObjectClass != ObjectClass.Corpse)
- {
- return false;
- }
-
- // Check if it has item slots (is a real container)
- try
- {
- if (containerWO.Values(LongValueKey.ItemSlots) < 48)
- return false;
- }
- catch
- {
- return false;
- }
-
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private ContainerType GetContainerType(int container)
- {
- try
- {
- var wo = core.WorldFilter[container];
- switch (wo.ObjectClass)
- {
- case ObjectClass.Container:
- return ContainerType.Chest;
- case ObjectClass.Corpse:
- if (wo.Name == $"Corpse of {core.CharacterFilter.Name}")
- return ContainerType.MyCorpse;
- else
- return ContainerType.MonsterCorpse;
- }
- return ContainerType.Unknown;
- }
- catch
- {
- return ContainerType.Unknown;
- }
- }
-
- private bool IsContainerPlayer(int container)
- {
- try
- {
- if (container == 0)
- return false;
-
- // Main pack
- if (core.WorldFilter[container].Id == core.CharacterFilter.Id)
- return true;
-
- // Side pack
- if (core.WorldFilter[container].Container == core.CharacterFilter.Id)
- return true;
-
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- private void UpdateContainerItems(int item, ItemState state)
- {
- try
- {
- if (!containerItems.ContainsKey(item))
- {
- containerItems.Add(item, state);
- }
- if (!containerItemsList.Contains(item))
- {
- containerItemsList.Add(item);
- }
- }
- catch (Exception ex)
- {
- LogError($"Error updating container items: {ex.Message}");
- }
- }
-
- private void LootedItem(int item)
- {
- try
- {
- if (containerItems.ContainsKey(item))
- {
- if (containerItems[item] != ItemState.Looted)
- {
- lootAttemptCount = 0;
- containerItems[item] = ItemState.Looted;
- }
- }
- }
- catch (Exception ex)
- {
- LogError($"Error marking item as looted: {ex.Message}");
- }
- }
-
- private bool HasMoreKeys(string keyName)
- {
- try
- {
- // Match UB's exact implementation - EXACT name match
- int count = 0;
- using (var inv = core.WorldFilter.GetInventory())
- {
- foreach (var wo in inv)
- {
- if (wo.Name == keyName) // EXACT match like UB
- {
- if (wo.Values(LongValueKey.StackCount, 0) > 0)
- {
- count += wo.Values(LongValueKey.StackCount);
- }
- else
- {
- count++;
- }
- }
- }
- }
- return count > 0;
- }
- catch (Exception ex)
- {
- LogError($"HasMoreKeys exception: {ex.Message}");
- return false;
- }
- }
-
- private string GetStatusString()
- {
- if (looterState == LooterState.Done)
- return "Ready";
-
- return $"{looterState} - {containerItems.Count} items";
- }
-
- private void UpdateStatus(string status)
- {
- StatusChanged?.Invoke(this, status);
- }
-
- private void Log(string message)
- {
- PluginCore.WriteToChat($"[ChestLooter] {message}");
- }
-
- private void LogError(string message)
- {
- PluginCore.WriteToChat($"[ChestLooter] ERROR: {message}");
- }
-
- #endregion
-
- #region IDisposable
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (!disposed)
- {
- if (disposing)
- {
- try
- {
- if (baseTimer != null)
- {
- baseTimer.Timeout -= BaseTimer_Timeout;
- baseTimer.Stop();
- baseTimer = null;
- }
- DisableDispatch();
- }
- catch (Exception ex)
- {
- LogError($"Error during disposal: {ex.Message}");
- }
- }
-
- disposed = true;
- }
- }
-
- ~ChestLooter()
- {
- Dispose(false);
- }
-
- #endregion
- }
-}
diff --git a/MosswartMassacre/ChestLooterSettings.cs b/MosswartMassacre/ChestLooterSettings.cs
deleted file mode 100644
index 517925a..0000000
--- a/MosswartMassacre/ChestLooterSettings.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using System;
-
-namespace MosswartMassacre
-{
- ///
- /// Settings for the Chest Looter feature
- /// These settings are persisted per-character via PluginSettings
- ///
- public class ChestLooterSettings
- {
- // Target configuration
- public string ChestName { get; set; } = "";
- public string KeyName { get; set; } = "";
-
- // Feature toggles
- public bool Enabled { get; set; } = false;
- public bool EnableChests { get; set; } = true;
- public bool AutoSalvageAfterLooting { get; set; } = false;
- public bool JumpWhenLooting { get; set; } = false;
- public bool BlockVtankMelee { get; set; } = false;
- public bool TestMode { get; set; } = false;
- public bool VerboseLogging { get; set; } = false;
-
- // Timing and retry settings
- public int DelaySpeed { get; set; } = 1000; // Delay for unlock/open/close in ms
- public int OverallSpeed { get; set; } = 100; // Overall looter tick rate in ms
- public int MaxUnlockAttempts { get; set; } = 10; // Max attempts to unlock chest
- public int MaxOpenAttempts { get; set; } = 10; // Max attempts to open chest
- public int AttemptsBeforeBlacklisting { get; set; } = 500; // Item loot attempts before giving up
-
- // Jump looting settings
- public int JumpHeight { get; set; } = 100; // Jump height (full bar is 1000)
-
- // UI state
- public bool ShowChestLooterTab { get; set; } = true;
-
- ///
- /// Constructor with default values
- ///
- public ChestLooterSettings()
- {
- // All defaults set via property initializers above
- }
-
- ///
- /// Validate settings and apply constraints
- ///
- public void Validate()
- {
- // Ensure OverallSpeed isn't too fast (can cause issues)
- if (OverallSpeed < 100)
- OverallSpeed = 100;
-
- // Ensure delays are reasonable
- if (DelaySpeed < 500)
- DelaySpeed = 500;
-
- // Ensure attempt limits are positive
- if (MaxUnlockAttempts < 1)
- MaxUnlockAttempts = 1;
- if (MaxOpenAttempts < 1)
- MaxOpenAttempts = 1;
- if (AttemptsBeforeBlacklisting < 1)
- AttemptsBeforeBlacklisting = 1;
-
- // Clamp jump height to reasonable range
- if (JumpHeight < 0)
- JumpHeight = 0;
- if (JumpHeight > 1000)
- JumpHeight = 1000;
- }
-
- ///
- /// Reset all settings to default values
- ///
- public void Reset()
- {
- ChestName = "";
- KeyName = "";
- Enabled = false;
- EnableChests = true;
- AutoSalvageAfterLooting = false;
- JumpWhenLooting = false;
- BlockVtankMelee = false;
- TestMode = false;
- VerboseLogging = false;
- DelaySpeed = 1000;
- OverallSpeed = 100;
- MaxUnlockAttempts = 10;
- MaxOpenAttempts = 10;
- AttemptsBeforeBlacklisting = 500;
- JumpHeight = 100;
- ShowChestLooterTab = true;
- }
-
- ///
- /// Create a copy of these settings
- ///
- public ChestLooterSettings Clone()
- {
- return new ChestLooterSettings
- {
- ChestName = this.ChestName,
- KeyName = this.KeyName,
- Enabled = this.Enabled,
- EnableChests = this.EnableChests,
- AutoSalvageAfterLooting = this.AutoSalvageAfterLooting,
- JumpWhenLooting = this.JumpWhenLooting,
- BlockVtankMelee = this.BlockVtankMelee,
- TestMode = this.TestMode,
- VerboseLogging = this.VerboseLogging,
- DelaySpeed = this.DelaySpeed,
- OverallSpeed = this.OverallSpeed,
- MaxUnlockAttempts = this.MaxUnlockAttempts,
- MaxOpenAttempts = this.MaxOpenAttempts,
- AttemptsBeforeBlacklisting = this.AttemptsBeforeBlacklisting,
- JumpHeight = this.JumpHeight,
- ShowChestLooterTab = this.ShowChestLooterTab
- };
- }
- }
-}
diff --git a/MosswartMassacre/ClientTelemetry.cs b/MosswartMassacre/ClientTelemetry.cs
deleted file mode 100644
index 78a9791..0000000
--- a/MosswartMassacre/ClientTelemetry.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Diagnostics;
-using System.Threading;
-using System;
-
-public class ClientTelemetry
-{
- private readonly Process _proc;
-
- public ClientTelemetry()
- {
- _proc = Process.GetCurrentProcess();
- }
-
- /// Working-set memory in bytes.
- public long MemoryBytes => _proc.WorkingSet64;
-
- /// Total open handles.
- public int HandleCount => _proc.HandleCount;
-
- /// CPU utilisation (%) averaged over .
- public float GetCpuUsage(int sampleMs = 500)
- {
- // you can keep your PerformanceCounter variant, but here’s a simpler PID-based way:
- var startCpu = _proc.TotalProcessorTime;
- var start = DateTime.UtcNow;
- Thread.Sleep(sampleMs);
- var endCpu = _proc.TotalProcessorTime;
- var end = DateTime.UtcNow;
-
- // CPU‐time used across all cores:
- var cpuMs = (endCpu - startCpu).TotalMilliseconds;
- var elapsedMs = (end - start).TotalMilliseconds * Environment.ProcessorCount;
- return (float)(cpuMs / elapsedMs * 100.0);
- }
-}
diff --git a/MosswartMassacre/CommandRouter.cs b/MosswartMassacre/CommandRouter.cs
deleted file mode 100644
index edba553..0000000
--- a/MosswartMassacre/CommandRouter.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MosswartMassacre
-{
- ///
- /// Dictionary-based /mm command dispatcher. Commands are registered with descriptions
- /// and routed by name lookup instead of a giant switch statement.
- ///
- internal class CommandRouter
- {
- private readonly Dictionary handler, string description)> _commands
- = new Dictionary, string)>(StringComparer.OrdinalIgnoreCase);
-
- ///
- /// Register a command with its handler and help description.
- ///
- internal void Register(string name, Action handler, string description)
- {
- _commands[name] = (handler, description);
- }
-
- ///
- /// Dispatch a raw /mm command string. Returns false if the command was not found.
- ///
- internal bool Dispatch(string rawText)
- {
- string[] args = rawText.Substring(3).Trim().Split(' ');
-
- if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
- {
- PluginCore.WriteToChat("Usage: /mm . Try /mm help");
- return true;
- }
-
- string subCommand = args[0].ToLower();
-
- if (subCommand == "help")
- {
- PrintHelp();
- return true;
- }
-
- if (_commands.TryGetValue(subCommand, out var entry))
- {
- entry.handler(args);
- return true;
- }
-
- PluginCore.WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
- return false;
- }
-
- private void PrintHelp()
- {
- PluginCore.WriteToChat("Mosswart Massacre Commands:");
- foreach (var kvp in _commands)
- {
- if (!string.IsNullOrEmpty(kvp.Value.description))
- {
- PluginCore.WriteToChat($"/mm {kvp.Key,-18} - {kvp.Value.description}");
- }
- }
- }
- }
-}
diff --git a/MosswartMassacre/Constants.cs b/MosswartMassacre/Constants.cs
deleted file mode 100644
index c944d43..0000000
--- a/MosswartMassacre/Constants.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace MosswartMassacre
-{
- ///
- /// Centralized constants for timer intervals, message type IDs, and property keys.
- ///
- internal static class Constants
- {
- // Timer intervals (milliseconds)
- internal const int StatsUpdateIntervalMs = 1000;
- internal const int VitalsUpdateIntervalMs = 5000;
- internal const int CommandProcessIntervalMs = 10;
- internal const int QuestStreamingIntervalMs = 30000;
- internal const int CharacterStatsIntervalMs = 600000; // 10 minutes
- internal const int LoginDelayMs = 5000;
-
- // Network message types
- internal const int GameEventMessageType = 0xF7B0;
- internal const int PrivateUpdatePropertyInt64 = 0x02CF;
-
- // Game event IDs (sub-events within 0xF7B0)
- internal const int AllegianceInfoEvent = 0x0020;
- internal const int LoginCharacterEvent = 0x0013;
- internal const int TitlesListEvent = 0x0029;
- internal const int SetTitleEvent = 0x002b;
-
- // Int64 property keys
- internal const int AvailableLuminanceKey = 6;
- internal const int MaximumLuminanceKey = 7;
-
- ///
- /// Plugin version derived from assembly version (CalVer: YYYY.M.D.HHmm)
- ///
- public static string PluginVersion =>
- typeof(Constants).Assembly.GetName().Version.ToString();
- }
-}
diff --git a/MosswartMassacre/DecalHarmonyClean.cs b/MosswartMassacre/DecalHarmonyClean.cs
deleted file mode 100644
index 0bcdf23..0000000
--- a/MosswartMassacre/DecalHarmonyClean.cs
+++ /dev/null
@@ -1,559 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using Harmony; // UtilityBelt's Harmony 1.2.0.1 uses old Harmony namespace
-
-namespace MosswartMassacre
-{
- ///
- /// Clean Harmony implementation using UtilityBelt's exact pattern
- /// Uses same namespace convention, same API, same Harmony package (Lib.Harmony 2.2.2)
- ///
- public static class DecalHarmonyClean
- {
- // Use UtilityBelt's namespace pattern
- private static readonly string harmonyNamespace = "com.mosswartmassacre.decal";
- private static HarmonyInstance harmonyDecal;
- private static bool patchesApplied = false;
- internal static int messagesIntercepted = 0;
-
- // Debug logging for troubleshooting
- private static readonly Queue debugLog = new Queue();
- private const int MAX_DEBUG_ENTRIES = 50;
-
- ///
- /// Initialize Harmony patches using UtilityBelt's exact pattern
- ///
- public static bool Initialize()
- {
- try
- {
- // Use UtilityBelt's exact pattern: lazy initialization check + new Harmony()
- if (harmonyDecal == null)
- harmonyDecal = HarmonyInstance.Create(harmonyNamespace + ".patches");
-
- // Apply patches using UtilityBelt's approach
- ApplyDecalPatches();
-
- return patchesApplied;
- }
- catch (Exception ex)
- {
- // Only log critical initialization failures
- PluginCore.WriteToChat($"[DECAL] Critical initialization error: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// Apply patches to DECAL's AddChatText methods using UtilityBelt approach
- ///
- private static void ApplyDecalPatches()
- {
- try
- {
- // PATHWAY 1: Target HooksWrapper.AddChatText
- PatchHooksWrapper();
-
- // PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
- PatchHostActions();
- }
- catch (Exception ex)
- {
- AddDebugLog($"ApplyDecalPatches failed: {ex.Message}");
- }
- }
-
- ///
- /// Patch HooksWrapper.AddChatText methods
- ///
- private static void PatchHooksWrapper()
- {
- try
- {
- Type hooksWrapperType = typeof(HooksWrapper);
-
- var allAddChatTextMethods = hooksWrapperType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
- .Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw").ToArray();
-
- foreach (var method in allAddChatTextMethods)
- {
- var parameters = method.GetParameters();
-
- string prefixMethodName = parameters.Length == 2 ? "AddChatTextPrefixHooks" :
- parameters.Length == 3 ? "AddChatTextPrefixHooks3" :
- "AddChatTextPrefixHooksGeneric";
-
- try
- {
- ApplySinglePatch(method, prefixMethodName);
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchHooksWrapper single patch failed ({prefixMethodName}): {ex.Message}");
- }
- }
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchHooksWrapper failed: {ex.Message}");
- }
- }
-
- ///
- /// Patch Host.Actions.AddChatText methods (what our plugin uses)
- ///
- private static void PatchHostActions()
- {
- try
- {
- if (PluginCore.MyHost?.Actions == null)
- {
- return;
- }
-
- Type actionsType = PluginCore.MyHost.Actions.GetType();
-
- // Check if Host.Actions is already a HooksWrapper (to avoid double patching)
- if (actionsType == typeof(HooksWrapper))
- {
- // PATHWAY 3: Try to patch at PluginHost level
- PatchPluginHost();
- return;
- }
-
- var hostAddChatTextMethods = actionsType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
- .Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw").ToArray();
-
- foreach (var method in hostAddChatTextMethods)
- {
- var parameters = method.GetParameters();
-
- string prefixMethodName = parameters.Length == 3 ? "AddChatTextPrefixHost" :
- parameters.Length == 4 ? "AddChatTextPrefixHost4" :
- "AddChatTextPrefixHostGeneric";
-
- try
- {
- ApplySinglePatch(method, prefixMethodName);
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchHostActions single patch failed ({prefixMethodName}): {ex.Message}");
- }
- }
-
- // PATHWAY 3: Try to patch at PluginHost level
- PatchPluginHost();
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchHostActions failed: {ex.Message}");
- }
- }
-
- ///
- /// Try a different approach - patch the actual chat system or find global instances
- ///
- private static void PatchPluginHost()
- {
- try
- {
- var coreActions = CoreManager.Current?.Actions;
- if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
- {
- var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
- .Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
-
- foreach (var method in coreActionsMethods)
- {
- var parameters = method.GetParameters();
-
- try
- {
- string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
- ApplySinglePatch(method, prefixMethodName);
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchPluginHost single patch failed: {ex.Message}");
- }
- }
- }
- }
- catch (Exception ex)
- {
- AddDebugLog($"PatchPluginHost failed: {ex.Message}");
- }
- }
-
- ///
- /// Apply a single patch using UtilityBelt's method
- ///
- private static void ApplySinglePatch(MethodInfo targetMethod, string prefixMethodName)
- {
- try
- {
- // Get our prefix method
- var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
- BindingFlags.Static | BindingFlags.Public);
-
- if (prefixMethod != null)
- {
- // Use UtilityBelt's exact approach
- harmonyDecal.Patch(targetMethod, new HarmonyMethod(prefixMethod));
- patchesApplied = true;
- }
- }
- catch (Exception ex)
- {
- AddDebugLog($"ApplySinglePatch failed ({prefixMethodName}): {ex.Message}");
- }
- }
-
- ///
- /// List available methods for debugging
- ///
- private static void ListAvailableMethods(Type type)
- {
- var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
- .Where(m => m.Name == "AddChatText").ToArray();
-
- foreach (var method in methods)
- {
- var parameters = method.GetParameters();
- string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name));
- }
- }
-
- ///
- /// Clean up patches using UtilityBelt's pattern
- ///
- public static void Cleanup()
- {
- try
- {
- if (harmonyDecal != null && patchesApplied)
- {
- // Use UtilityBelt's cleanup pattern
- harmonyDecal.UnpatchAll(harmonyNamespace + ".patches");
- }
- patchesApplied = false;
- }
- catch (Exception ex)
- {
- AddDebugLog($"Cleanup failed: {ex.Message}");
- }
- }
-
- ///
- /// Status checks
- ///
- public static bool IsActive() => patchesApplied && harmonyDecal != null;
- public static int GetMessagesIntercepted() => messagesIntercepted;
-
- ///
- /// Add debug log entry
- ///
- public static void AddDebugLog(string message)
- {
- lock (debugLog)
- {
- debugLog.Enqueue($"{DateTime.Now:HH:mm:ss.fff} {message}");
- while (debugLog.Count > MAX_DEBUG_ENTRIES)
- {
- debugLog.Dequeue();
- }
- }
- }
-
- ///
- /// Get debug log entries
- ///
- public static string[] GetDebugLog()
- {
- lock (debugLog)
- {
- return debugLog.ToArray();
- }
- }
- }
-
- ///
- /// Patch methods for DECAL interception using UtilityBelt's approach
- ///
- public static class DecalPatchMethods
- {
- ///
- /// Prefix method for HooksWrapper.AddChatText(string, int) - intercepts plugin messages via HooksWrapper
- ///
- public static bool AddChatTextPrefixHooks(string text, int color)
- {
- try
- {
- // Always increment to verify patch is working
- DecalHarmonyClean.messagesIntercepted++;
-
- if (PluginCore.AggressiveChatStreamingEnabled)
- {
- }
-
- // Process ALL messages (including our own) for streaming
- if (!string.IsNullOrEmpty(text))
- {
- // Process the intercepted message
- ProcessInterceptedMessage(text, color, "HooksWrapper-2param");
-
- // Silent operation - debug output removed
- }
-
- // Always return true to let the original AddChatText continue
- return true;
- }
- catch
- {
- // Never let our interception break other plugins
- return true;
- }
- }
-
- ///
- /// Prefix method for HooksWrapper.AddChatText(string, int, int) - 3-parameter version
- ///
- public static bool AddChatTextPrefixHooks3(string text, int color, int target)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (!string.IsNullOrEmpty(text))
- {
- ProcessInterceptedMessage(text, color, $"HooksWrapper-3param(target={target})");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- ///
- /// Generic prefix method for any other HooksWrapper AddChatText overloads
- ///
- public static bool AddChatTextPrefixHooksGeneric(string text)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (!string.IsNullOrEmpty(text))
- {
- ProcessInterceptedMessage(text, 0, "HooksWrapper-generic");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- ///
- /// Process intercepted plugin messages
- ///
- private static void ProcessInterceptedMessage(string text, int color, string source)
- {
- try
- {
- // Identify source plugin
- string sourcePlugin = IdentifySourcePlugin(text);
-
- // Add timestamp
- var timestamp = DateTime.Now.ToString("HH:mm:ss");
- var fullMessage = $"{timestamp} [{sourcePlugin}] {text}";
-
- // Debug logging
-
- // Stream to WebSocket if both debug streaming AND WebSocket are enabled
- if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled)
- {
- Task.Run(() => WebSocket.SendChatTextAsync(color, text));
- }
- }
- catch (Exception ex)
- {
- DecalHarmonyClean.AddDebugLog($"ProcessInterceptedMessage failed: {ex.Message}");
- }
- }
-
- ///
- /// Identify which plugin sent the message using UtilityBelt's patterns
- ///
- private static string IdentifySourcePlugin(string text)
- {
- // Known plugin prefixes
- if (text.StartsWith("[VTank]")) return "VTank";
- if (text.StartsWith("[UB]") || text.Contains("UtilityBelt")) return "UtilityBelt";
- if (text.StartsWith("[VGI]")) return "VGI";
- if (text.StartsWith("[VI]")) return "VirindiIntegrator";
- if (text.StartsWith("[GoArrow]")) return "GoArrow";
- if (text.StartsWith("[Meta]")) return "Meta";
- if (text.StartsWith("[VTClassic]")) return "VTClassic";
-
- // Pattern-based detection for messages without prefixes
- if (text.Contains("Macro started") || text.Contains("Macro stopped")) return "VTank";
- if (text.Contains("Quest data updated")) return "UtilityBelt";
- if (text.Contains("Database read complete")) return "VGI";
- if (text.Contains("Disconnected, retry")) return "VI";
-
- return "Unknown";
- }
-
- // ===== HOST.ACTIONS PREFIX METHODS =====
-
- ///
- /// Prefix method for Host.Actions.AddChatText(string, int, int) - 3-parameter version
- ///
- public static bool AddChatTextPrefixHost(string text, int color, int target)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (PluginCore.AggressiveChatStreamingEnabled)
- {
- }
-
- if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
- {
- ProcessInterceptedMessage(text, color, $"Host.Actions-3param(target={target})");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- ///
- /// Prefix method for Host.Actions.AddChatText(string, int, int, int) - 4-parameter version
- ///
- public static bool AddChatTextPrefixHost4(string text, int color, int target, int window)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
- {
- ProcessInterceptedMessage(text, color, $"Host.Actions-4param(target={target},window={window})");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- ///
- /// Generic prefix method for any other Host.Actions.AddChatText overloads
- ///
- public static bool AddChatTextPrefixHostGeneric(string text)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
- {
- ProcessInterceptedMessage(text, 0, "Host.Actions-generic");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- // ===== COREMANAGER.ACTIONS PREFIX METHODS =====
-
- ///
- /// Prefix method for CoreManager.Actions methods - trying different instances
- ///
- public static bool AddChatTextPrefixCore2(string text, int color)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (PluginCore.AggressiveChatStreamingEnabled)
- {
- }
-
- if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
- {
- ProcessInterceptedMessage(text, color, "CoreActions-2param");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
-
- ///
- /// Prefix method for CoreManager.Actions 3-param methods
- ///
- public static bool AddChatTextPrefixCore3(string text, int color, int target)
- {
- try
- {
- DecalHarmonyClean.messagesIntercepted++;
-
- if (PluginCore.AggressiveChatStreamingEnabled)
- {
- }
-
- if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
- {
- ProcessInterceptedMessage(text, color, $"CoreActions-3param(target={target})");
-
- // Silent operation - debug output removed
- }
-
- return true;
- }
- catch
- {
- return true;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/DelayedCommandManager.cs b/MosswartMassacre/DelayedCommandManager.cs
index bf7a400..4976e4b 100644
--- a/MosswartMassacre/DelayedCommandManager.cs
+++ b/MosswartMassacre/DelayedCommandManager.cs
@@ -28,8 +28,8 @@ namespace MosswartMassacre
{
while (delayedCommands.Count > 0 && delayedCommands[0].RunAt <= DateTime.UtcNow)
{
- // Use Decal_DispatchOnChatCommand to ensure other plugins can intercept
- PluginCore.DispatchChatToBoxWithPluginIntercept(delayedCommands[0].Command);
+ PluginCore.WriteToChat($"[Debug] Executing delayed: {delayedCommands[0].Command}");
+ CoreManager.Current.Actions.InvokeChatParser(delayedCommands[0].Command);
delayedCommands.RemoveAt(0);
}
diff --git a/MosswartMassacre/FlagTrackerData.cs b/MosswartMassacre/FlagTrackerData.cs
deleted file mode 100644
index 9ac261d..0000000
--- a/MosswartMassacre/FlagTrackerData.cs
+++ /dev/null
@@ -1,1932 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using Decal.Filters;
-using Mag.Shared.Constants;
-
-namespace MosswartMassacre
-{
- ///
- /// Data management class for Flag Tracker
- /// Ported from UBS Lua flagtracker data structures
- ///
- public class FlagTrackerData : IDisposable
- {
- public FlagTrackerData()
- {
- InitializeDataStructures();
- }
- #region Augmentation Data Structures
- public class AugmentationInfo
- {
- public string Name { get; set; }
- public int? IntId { get; set; }
- public int Repeatable { get; set; }
- public string Trainer { get; set; }
- public string Location { get; set; }
- public int CurrentValue { get; set; }
- public bool IsMaxed => CurrentValue >= Repeatable;
- }
-
- public Dictionary> AugmentationCategories { get; private set; }
-
- #endregion
-
- #region Luminance Aura Data Structures
- public class LuminanceAuraInfo
- {
- public string Name { get; set; }
- public int IntId { get; set; }
- public int Cap { get; set; }
- public string QuestFlag { get; set; } // For Seer auras only
- public int CurrentValue { get; set; }
- }
-
- public Dictionary> LuminanceAuraCategories { get; private set; }
- #endregion
-
- #region Recall Spell Data Structures
- public class RecallSpellInfo
- {
- public string Name { get; set; }
- public int SpellId { get; set; }
- public bool IsKnown { get; set; }
- public int IconId { get; set; }
- public string Category { get; set; }
- }
-
- public List RecallSpells { get; private set; }
- #endregion
-
- #region Society Quest Data Structures
- public class SocietyQuestInfo
- {
- public string Name { get; set; }
- public string StartTag { get; set; }
- public string EndTag { get; set; }
- public QuestType Type { get; set; }
- public object[] ExtraData { get; set; } // For type-specific data
- }
-
- public enum QuestType
- {
- Other = 0,
- KillTask = 1,
- CollectItem = 2,
- QuestTag = 3,
- MultiQuestTag = 4
- }
-
- public Dictionary> SocietyQuests { get; private set; }
- #endregion
-
- #region Character Flag Data Structures
- public class CharacterFlagInfo
- {
- public string Name { get; set; }
- public string QuestFlag { get; set; }
- public QuestInfoType InfoType { get; set; }
- }
-
- public enum QuestInfoType
- {
- SolveCount = 1,
- ReadyCheck = 2,
- StampCheck = 3
- }
-
- public Dictionary> CharacterFlags { get; private set; }
- #endregion
-
- #region Cantrip Data Structures
- public class CantripInfo
- {
- public string Name { get; set; }
- public string Value { get; set; } // "Minor", "Major", "Epic", etc.
- public System.Drawing.Color Color { get; set; }
- public int? IconId { get; set; } // Skill icon from character skills
- public int? SpellIconId { get; set; } // Spell icon (for attributes)
- public int? BackgroundIconId { get; set; } // Background icon (for attributes)
- public int ComputedIconId { get; set; } // Final resolved icon for display
- }
-
- public Dictionary> Cantrips { get; private set; }
-
- // Skill name mappings for cantrips that have different names than their skills
- private readonly Dictionary SkillCantripReplacements = new Dictionary
- {
- [15] = "MagicResistance", // MagicDefense
- [6] = "Invulnerability", // MeleeDefense
- [7] = "Impgrenability", // MissileDefense
- [47] = "MissileWeapon", // MissileWeapons
- [44] = "HeavyWeapon", // HeavyWeapons
- [45] = "LightWeapon", // LightWeapons
- [46] = "FinesseWeapon" // FinesseWeapons
- };
- #endregion
-
- #region Weapon Data Structures (Legacy - to be removed)
- // Old weapon structure - keeping for compatibility but unused
- #endregion
-
- #region Cached Data
- private DateTime lastUpdateTime = DateTime.MinValue;
- private Dictionary cachedValues = new Dictionary();
- #endregion
-
- #region Initialization
- private void InitializeDataStructures()
- {
- InitializeAugmentationData();
- InitializeLuminanceAuraData();
- InitializeRecallSpellData();
- InitializeSocietyQuestData();
- InitializeCharacterFlagData();
- InitializeCantripData();
- InitializeNewWeaponData();
- }
-
- private void InitializeAugmentationData()
- {
- AugmentationCategories = new Dictionary>
- {
- ["Death Augs"] = new List
- {
- new AugmentationInfo { Name = "Keep Items", IntId = 231, Repeatable = 3, Trainer = "Rohula bint Ludun", Location = "Ayan Baqur" }, // AugmentationLessDeathItemLoss
- new AugmentationInfo { Name = "Keep Spells", IntId = 232, Repeatable = 1, Trainer = "Erik Festus", Location = "Ayan Baqur" } // AugmentationSpellsRemainPastDeath
- },
- ["Skill Augs"] = new List
- {
- new AugmentationInfo { Name = "+5 All Skills", IntId = 326, Repeatable = 1, Trainer = "Arianna the Adept", Location = "Bandit Castle" }, // AugmentationJackOfAllTrades
- new AugmentationInfo { Name = "+10 Melee Skills", IntId = 300, Repeatable = 1, Trainer = "Carlito Gallo", Location = "Silyun" }, // AugmentationSkilledMelee
- new AugmentationInfo { Name = "+10 Magic Skills", IntId = 302, Repeatable = 1, Trainer = "Rahina bint Zalanis", Location = "Zaikhal" }, // AugmentationSkilledMagic
- new AugmentationInfo { Name = "+10 Missile Skills", IntId = 301, Repeatable = 1, Trainer = "Kilaf", Location = "Zaikhal" } // AugmentationSkilledMissile
- },
- ["Rating Augs"] = new List
- {
- new AugmentationInfo { Name = "25% Crit Protection", IntId = 233, Repeatable = 1, Trainer = "Piersanti Linante", Location = "Sanamar" }, // AugmentationCriticalDefense
- new AugmentationInfo { Name = "1% Critical Chance", IntId = 298, Repeatable = 1, Trainer = "Anfram Mellow", Location = "Ayan Baqur" }, // AugmentationCriticalExpertise
- new AugmentationInfo { Name = "3% Critical Damage", IntId = 299, Repeatable = 1, Trainer = "Alishia bint Aldan", Location = "Ayan Baqur" }, // AugmentationCriticalPower
- new AugmentationInfo { Name = "3% Damage Rating", IntId = 309, Repeatable = 1, Trainer = "Neela Nashua", Location = "Bandit Castle" }, // AugmentationDamageBonus
- new AugmentationInfo { Name = "3% Damage Reduction", IntId = 310, Repeatable = 1, Trainer = "Emily Yarow", Location = "Cragstone" } // AugmentationDamageReduction
- },
- ["Burden / Pack Augs"] = new List
- {
- new AugmentationInfo { Name = "Extra Carrying Capacity", IntId = 230, Repeatable = 5, Trainer = "Husoon", Location = "Zaikhal" }, // AugmentationIncreasedCarryingCapacity
- new AugmentationInfo { Name = "Extra Pack Slot", IntId = 229, Repeatable = 1, Trainer = "Dumida bint Ruminre", Location = "Zaikhal" }, // AugmentationExtraPackSlot
- new AugmentationInfo { Name = "Infused War Magic", IntId = 297, Repeatable = 1, Trainer = "Raphel Detante", Location = "Silyun" }, // AugmentationInfusedWarMagic
- new AugmentationInfo { Name = "Infused Void Magic", IntId = 328, Repeatable = 1, Trainer = "Morathe", Location = "Candeth Keep" }, // AugmentationInfusedVoidMagic
- new AugmentationInfo { Name = "Infused Creature Magic", IntId = 294, Repeatable = 1, Trainer = "Gustuv Lansdown", Location = "Cragstone" }, // AugmentationInfusedCreatureMagic
- new AugmentationInfo { Name = "Infused Life Magic", IntId = 296, Repeatable = 1, Trainer = "Akemi Fei", Location = "Hebian-To" }, // AugmentationInfusedLifeMagic
- new AugmentationInfo { Name = "Infused Item Magic", IntId = 295, Repeatable = 1, Trainer = "Gan Fo", Location = "Hebian-To" } // AugmentationInfusedItemMagic
- },
- ["Misc Augs"] = new List
- {
- new AugmentationInfo { Name = "10% Health Increase", IntId = null, Repeatable = 1, Trainer = "Donatello Linante", Location = "Silyun" }, // No specific property
- new AugmentationInfo { Name = "Increased Spell Duration", IntId = 238, Repeatable = 5, Trainer = "Nawamara Ujio", Location = "Mayoi" }, // AugmentationIncreasedSpellDuration
- new AugmentationInfo { Name = "Faster HP Regen", IntId = 237, Repeatable = 2, Trainer = "Alison Dulane", Location = "Bandit Castle" }, // AugmentationFasterRegen
- new AugmentationInfo { Name = "5% Experience Increase", IntId = 234, Repeatable = 1, Trainer = "Rickard Dumalia", Location = "Silyun" } // AugmentationBonusXp
- },
- ["Salvage Augs"] = new List
- {
- new AugmentationInfo { Name = "Specialized Weapon Tinkering", IntId = 228, Repeatable = 1, Trainer = "Lenor Turk", Location = "Cragstone" }, // AugmentationSpecializeWeaponTinkering
- new AugmentationInfo { Name = "Specialized Armor Tinkering", IntId = 226, Repeatable = 1, Trainer = "Joshun Felden", Location = "Cragstone" }, // AugmentationSpecializeArmorTinkering
- new AugmentationInfo { Name = "Specialized Item Tinkering", IntId = 225, Repeatable = 1, Trainer = "Brienne Carlus", Location = "Cragstone" }, // AugmentationSpecializeItemTinkering
- new AugmentationInfo { Name = "Specialized Magic Item Tinkering", IntId = 227, Repeatable = 1, Trainer = "Burrell Sammrun", Location = "Cragstone" }, // AugmentationSpecializeMagicItemTinkering
- new AugmentationInfo { Name = "Specialized Salvaging", IntId = 224, Repeatable = 1, Trainer = "Robert Crow", Location = "Cragstone" }, // AugmentationSpecializeSalvaging
- new AugmentationInfo { Name = "25% More Salvage", IntId = 235, Repeatable = 4, Trainer = "Kris Cennis", Location = "Cragstone" }, // AugmentationBonusSalvage
- new AugmentationInfo { Name = "5% Imbue Chance", IntId = 236, Repeatable = 1, Trainer = "Lug", Location = "Oolutanga's Refuge" } // AugmentationBonusImbueChance
- },
- ["Stat Augs"] = new List
- {
- new AugmentationInfo { Name = "All Stats", IntId = 217, Repeatable = 10, Trainer = "", Location = "" }, // AugmentationInnateFamily
- new AugmentationInfo { Name = "Strength", IntId = 218, Repeatable = 10, Trainer = "Fiun Luunere", Location = "Fiun Outpost" }, // AugmentationInnateStrength
- new AugmentationInfo { Name = "Endurance", IntId = 219, Repeatable = 10, Trainer = "Fiun Ruun", Location = "Fiun Outpost" }, // AugmentationInnateEndurance
- new AugmentationInfo { Name = "Coordination", IntId = 220, Repeatable = 10, Trainer = "Fiun Bayaas", Location = "Fiun Outpost" }, // AugmentationInnateCoordination
- new AugmentationInfo { Name = "Quickness", IntId = 221, Repeatable = 10, Trainer = "Fiun Riish", Location = "Fiun Outpost" }, // AugmentationInnateQuickness
- new AugmentationInfo { Name = "Focus", IntId = 222, Repeatable = 10, Trainer = "Fiun Vasherr", Location = "Fiun Outpost" }, // AugmentationInnateFocus
- new AugmentationInfo { Name = "Self", IntId = 223, Repeatable = 10, Trainer = "Fiun Noress", Location = "Fiun Outpost" } // AugmentationInnateSelf
- },
- ["Resistance Augs"] = new List
- {
- new AugmentationInfo { Name = "All Resistances", IntId = 239, Repeatable = 2, Trainer = "", Location = "" }, // AugmentationResistanceFamily
- new AugmentationInfo { Name = "Blunt", IntId = 242, Repeatable = 2, Trainer = "Nawamara Dia", Location = "Hebian-To" }, // AugmentationResistanceBlunt
- new AugmentationInfo { Name = "Pierce", IntId = 241, Repeatable = 2, Trainer = "Kyujo Rujen", Location = "Hebian-To" }, // AugmentationResistancePierce
- new AugmentationInfo { Name = "Slashing", IntId = 240, Repeatable = 2, Trainer = "Ilin Wis", Location = "Hebian-To" }, // AugmentationResistanceSlash
- new AugmentationInfo { Name = "Fire", IntId = 244, Repeatable = 2, Trainer = "Rikshen Ri", Location = "Hebian-To" }, // AugmentationResistanceFire
- new AugmentationInfo { Name = "Frost", IntId = 245, Repeatable = 2, Trainer = "Lu Bao", Location = "Hebian-To" }, // AugmentationResistanceFrost
- new AugmentationInfo { Name = "Acid", IntId = 243, Repeatable = 2, Trainer = "Shujio Milao", Location = "Hebian-To" }, // AugmentationResistanceAcid
- new AugmentationInfo { Name = "Lightning", IntId = 246, Repeatable = 2, Trainer = "Enli Yuo", Location = "Hebian-To" } // AugmentationResistanceLightning
- }
- };
- }
-
- private void InitializeLuminanceAuraData()
- {
- LuminanceAuraCategories = new Dictionary>
- {
- ["Nalicana Auras"] = new List
- {
- new LuminanceAuraInfo { Name = "+1 Aetheria Proc Rating", IntId = 338, Cap = 5 }, // LumAugSurgeChanceRating
- new LuminanceAuraInfo { Name = "+1 Damage Reduction Rating", IntId = 334, Cap = 5 }, // LumAugDamageReductionRating
- new LuminanceAuraInfo { Name = "+1 Crit Reduction Rating", IntId = 336, Cap = 5 }, // LumAugCritReductionRating
- new LuminanceAuraInfo { Name = "+1 Damage Rating", IntId = 333, Cap = 5 }, // LumAugDamageRating
- new LuminanceAuraInfo { Name = "+1 Crit Damage Rating", IntId = 335, Cap = 5 }, // LumAugCritDamageRating
- new LuminanceAuraInfo { Name = "+1 Heal Rating", IntId = 342, Cap = 5 }, // LumAugHealingRating
- new LuminanceAuraInfo { Name = "+1 Equipment Mana Rating", IntId = 339, Cap = 5 }, // LumAugItemManaUsage
- new LuminanceAuraInfo { Name = "+1 Mana Stone Rating", IntId = 340, Cap = 5 }, // LumAugItemManaGain
- new LuminanceAuraInfo { Name = "+1 Crafting Skills", IntId = 343, Cap = 5 }, // LumAugSkilledCraft
- new LuminanceAuraInfo { Name = "+1 All Skills", IntId = 365, Cap = 10 } // LumAugAllSkills
- },
- ["Seer Auras"] = new List
- {
- new LuminanceAuraInfo { Name = "(Ka'hiri) +2 Specialized Skills", IntId = 344, Cap = 5, QuestFlag = "LoyalToKahiri" }, // LumAugSkilledSpec
- new LuminanceAuraInfo { Name = "(Ka'hiri) +1 Damage Rating", IntId = 333, Cap = 5, QuestFlag = "LoyalToKahiri" }, // LumAugDamageRating
- new LuminanceAuraInfo { Name = "(Shade of Lady Adja) +2 Specialized Skills", IntId = 344, Cap = 5, QuestFlag = "LoyalToShadeOfLadyAdja" }, // LumAugSkilledSpec
- new LuminanceAuraInfo { Name = "(Shade of Lady Adja) +1 Damage Reduction Rating", IntId = 334, Cap = 5, QuestFlag = "LoyalToShadeOfLadyAdja" }, // LumAugDamageReductionRating
- new LuminanceAuraInfo { Name = "(Liam of Gelid) +1 Damage Rating", IntId = 333, Cap = 5, QuestFlag = "LoyalToLiamOfGelid" }, // LumAugDamageRating
- new LuminanceAuraInfo { Name = "(Liam of Gelid) +1 Crit Damage Rating", IntId = 335, Cap = 5, QuestFlag = "LoyalToLiamOfGelid" }, // LumAugCritDamageRating
- new LuminanceAuraInfo { Name = "(Lord Tyragar) +1 Crit Reduction Rating", IntId = 336, Cap = 5, QuestFlag = "LoyalToLordTyragar" }, // LumAugCritReductionRating
- new LuminanceAuraInfo { Name = "(Lord Tyragar) +1 Damage Reduction Rating", IntId = 334, Cap = 5, QuestFlag = "LoyalToLordTyragar" } // LumAugDamageReductionRating
- }
- };
- }
-
- private void InitializeRecallSpellData()
- {
- RecallSpells = new List
- {
- new RecallSpellInfo { Name = "Recall the Sanctuary", SpellId = 2023, IconId = 0, Category = "Basic Recalls" },
- new RecallSpellInfo { Name = "Aerlinthe Recall", SpellId = 2041, IconId = 0, Category = "Island Recalls" },
- new RecallSpellInfo { Name = "Mount Lethe Recall", SpellId = 2813, IconId = 0, Category = "Island Recalls" },
- new RecallSpellInfo { Name = "Recall Aphus Lassel", SpellId = 2931, IconId = 0, Category = "Island Recalls" },
- new RecallSpellInfo { Name = "Ulgrim's Recall", SpellId = 2941, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Recall to the Singularity Caul", SpellId = 2943, IconId = 0, Category = "Island Recalls" },
- new RecallSpellInfo { Name = "Glenden Wood Recall", SpellId = 3865, IconId = 0, Category = "Town Recalls" },
- new RecallSpellInfo { Name = "Bur Recall", SpellId = 4084, IconId = 0, Category = "Town Recalls" },
- new RecallSpellInfo { Name = "Call of the Mhoire Forge", SpellId = 4128, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Paradox-touched Olthoi Infested Area Recall", SpellId = 4198, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Colosseum Recall", SpellId = 4213, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Return to the Keep", SpellId = 4214, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Gear Knight Invasion Area Camp Recall", SpellId = 5330, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Lost City of Neftet Recall", SpellId = 5541, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Rynthid Recall", SpellId = 6150, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Viridian Rise Recall", SpellId = 6321, IconId = 0, Category = "Special Recalls" },
- new RecallSpellInfo { Name = "Viridian Rise Great Tree Recall", SpellId = 6322, IconId = 0, Category = "Special Recalls" }
- };
- }
-
- private void InitializeSocietyQuestData()
- {
- SocietyQuests = new Dictionary>();
- // TODO: Initialize society quest data from Lua
- }
-
- private void InitializeCharacterFlagData()
- {
- CharacterFlags = new Dictionary>
- {
- ["Additional Skill Credits"] = new List
- {
- new CharacterFlagInfo { Name = "+1 Skill Lum Aura", QuestFlag = "lumaugskillquest", InfoType = QuestInfoType.SolveCount },
- new CharacterFlagInfo { Name = "+1 Skill Aun Ralirea", QuestFlag = "arantahkill1", InfoType = QuestInfoType.SolveCount },
- new CharacterFlagInfo { Name = "+1 Skill Chasing Oswald", QuestFlag = "oswaldmanualcompleted", InfoType = QuestInfoType.SolveCount }
- },
- ["Aetheria"] = new List
- {
- new CharacterFlagInfo { Name = "Blue Aetheria (75)", QuestFlag = "efulcentermanafieldused", InfoType = QuestInfoType.StampCheck },
- new CharacterFlagInfo { Name = "Yellow Aetheria (150)", QuestFlag = "efmlcentermanafieldused", InfoType = QuestInfoType.StampCheck },
- new CharacterFlagInfo { Name = "Red Aetheria (225)", QuestFlag = "efllcentermanafieldused", InfoType = QuestInfoType.StampCheck }
- }
- // TODO: Add remaining character flag categories
- };
- }
-
- private void InitializeCantripData()
- {
- Cantrips = new Dictionary>
- {
- ["Specialized Skills"] = new Dictionary(), // Dynamically populated
- ["Trained Skills"] = new Dictionary(), // Dynamically populated
- ["Attributes"] = new Dictionary(), // Dynamically populated when cantrips are detected
- ["Protection Auras"] = new Dictionary
- {
- // Pre-populate all protection auras so they show as red when missing
- ["Armor"] = new CantripInfo { Name = "Armor", Value = "N/A", Color = System.Drawing.Color.White },
- ["Bludgeoning Ward"] = new CantripInfo { Name = "Bludgeoning Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Piercing Ward"] = new CantripInfo { Name = "Piercing Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Slashing Ward"] = new CantripInfo { Name = "Slashing Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Flame Ward"] = new CantripInfo { Name = "Flame Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Frost Ward"] = new CantripInfo { Name = "Frost Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Acid Ward"] = new CantripInfo { Name = "Acid Ward", Value = "N/A", Color = System.Drawing.Color.White },
- ["Storm Ward"] = new CantripInfo { Name = "Storm Ward", Value = "N/A", Color = System.Drawing.Color.White }
- }
- };
- }
-
- #endregion
-
- #region Refresh Methods
- public void RefreshAll()
- {
- RefreshCachedData();
- RefreshAugmentations();
- RefreshLuminanceAuras();
- RefreshRecallSpells();
- RefreshSocietyQuests();
- RefreshFacilityHubQuests();
- RefreshCharacterFlags();
- RefreshCantrips();
- }
-
- public void RefreshCachedData()
- {
- try
- {
- if (CoreManager.Current?.CharacterFilter?.Name != null)
- {
- // Update cached values
- var character = CoreManager.Current.CharacterFilter;
- lastUpdateTime = DateTime.Now;
-
- // TODO: Implement cached data refresh
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing cached data: {ex.Message}");
- }
- }
-
- public void RefreshAugmentations()
- {
- try
- {
- if (CoreManager.Current?.CharacterFilter?.Name == null) return;
-
- var character = CoreManager.Current.CharacterFilter;
-
- // Update augmentation values
- foreach (var category in AugmentationCategories)
- {
- foreach (var aug in category.Value)
- {
- if (aug.IntId.HasValue)
- {
- // Get augmentation value from character data using DECAL API
- try
- {
- if (CoreManager.Current?.CharacterFilter != null)
- {
- var characterFilter = CoreManager.Current.CharacterFilter;
- var playerObject = CoreManager.Current.WorldFilter[characterFilter.Id];
-
- if (playerObject != null)
- {
- // Use CharacterFilter.GetCharProperty for character properties
- try
- {
- // DECAL API uses CharacterFilter.GetCharProperty for character properties
- aug.CurrentValue = characterFilter.GetCharProperty(aug.IntId.Value);
- }
- catch
- {
- // Try alternative access using reflection
- try
- {
- var valuesMethod = playerObject.GetType().GetMethod("Values", new Type[] { typeof(int) });
- if (valuesMethod != null)
- {
- aug.CurrentValue = (int)valuesMethod.Invoke(playerObject, new object[] { aug.IntId.Value });
- }
- else
- {
- aug.CurrentValue = 0;
- }
- }
- catch
- {
- aug.CurrentValue = 0;
- }
- }
- }
- else
- {
- aug.CurrentValue = 0;
- }
- }
- else
- {
- aug.CurrentValue = 0;
- }
- }
- catch
- {
- aug.CurrentValue = 0;
- }
- }
- else
- {
- // Handle special case for Asheron's Lesser Benediction (inventory count)
- if (aug.Name == "Asheron's Lesser Benediction")
- {
- // Count Asheron's Lesser Benediction items in inventory
- aug.CurrentValue = CountAsheronsLesserBenediction();
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing augmentations: {ex.Message}");
- }
- }
-
- private int CountAsheronsLesserBenediction()
- {
- try
- {
- int count = 0;
-
- // Search inventory for Asheron's Lesser Benediction
- foreach (WorldObject item in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (item.Name.Contains("Asheron's Lesser Benediction"))
- {
- // Use stack size for item count (default to 1 for non-stackable items)
- try
- {
- // Access stack size using reflection to avoid type issues
- var stackMethod = item.GetType().GetMethod("Values", new Type[] { typeof(int) });
- if (stackMethod != null)
- {
- int stackSize = (int)stackMethod.Invoke(item, new object[] { 12 }); // 12 = StackSize
- count += Math.Max(1, stackSize);
- }
- else
- {
- count += 1; // Default to 1
- }
- }
- catch
- {
- count += 1; // Default fallback
- }
- }
- }
-
- return count;
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error counting Asheron's Lesser Benediction: {ex.Message}");
- return 0;
- }
- }
-
- public void RefreshLuminanceAuras()
- {
- try
- {
- if (CoreManager.Current?.CharacterFilter?.Name == null) return;
-
- var characterFilter = CoreManager.Current.CharacterFilter;
-
- // Update luminance aura values
- foreach (var category in LuminanceAuraCategories)
- {
- foreach (var aura in category.Value)
- {
- try
- {
- // Use CharacterFilter.GetCharProperty for luminance auras
- aura.CurrentValue = characterFilter.GetCharProperty(aura.IntId);
- }
- catch
- {
- aura.CurrentValue = 0;
- }
- }
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing luminance auras: {ex.Message}");
- }
- }
-
- public void RefreshRecallSpells()
- {
- try
- {
- if (CoreManager.Current?.CharacterFilter?.Name == null) return;
-
- var characterFilter = CoreManager.Current.CharacterFilter;
- int knownCount = 0;
-
- // Check each recall spell to see if the character knows it
- foreach (var recall in RecallSpells)
- {
- try
- {
- // Use DECAL API to check if the character knows this spell
- recall.IsKnown = characterFilter.IsSpellKnown(recall.SpellId);
- if (recall.IsKnown) knownCount++;
-
- // Get spell icon from FileService if not already set
- if (recall.IconId == 0)
- {
- recall.IconId = GetSpellIcon(recall.SpellId);
- }
- }
- catch
- {
- recall.IsKnown = false;
- }
- }
-
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing recall spells: {ex.Message}");
- }
- }
-
- private int GetSpellIcon(int spellId)
- {
- try
- {
- // Try to get real spell icon first (matches original Lua approach)
- int realSpellIcon = GetRealSpellIcon(spellId);
- if (realSpellIcon != 0)
- {
- return realSpellIcon;
- }
-
- // Fallback to known recall spell icons
- var recallSpellIcons = new Dictionary
- {
- // Recall spell icons from AC spell data
- [2023] = 2943, // Recall the Sanctuary
- [2041] = 2814, // Aerlinthe Recall
- [2813] = 2813, // Mount Lethe Recall
- [2931] = 2931, // Recall Aphus Lassel
- [2943] = 2943, // Recall to the Singularity Caul
- [3865] = 3864, // Glenden Wood Recall
- [4084] = 4084, // Bur Recall
- [2941] = 2814, // Ulgrim's Recall (uses portal icon)
- [4128] = 4128, // Call of the Mhoire Forge
- [4198] = 4197, // Paradox-touched Olthoi Infested Area Recall
- [4213] = 4213, // Colosseum Recall
- [4214] = 4199, // Return to the Keep
- [5330] = 5175, // Gear Knight Invasion Area Camp Recall
- [5541] = 5541, // Lost City of Neftet Recall
- [6150] = 6150, // Rynthid Recall
- [6321] = 6321, // Viridian Rise Recall
- [6322] = 6322 // Viridian Rise Great Tree Recall
- };
-
- if (recallSpellIcons.ContainsKey(spellId))
- {
- // Add offset for spell icons
- return recallSpellIcons[spellId] + 0x6000000;
- }
-
- // Final fallback - default portal/recall icon
- return 0x6002D14;
- }
- catch
- {
- return 0x6002D14; // Default icon on error
- }
- }
-
- private int GetRealSpellIcon(int spellId)
- {
- try
- {
- // Method 1: Use DECAL FileService SpellTable directly (proper API approach)
- try
- {
- var fileService = CoreManager.Current.Filter();
- if (fileService?.SpellTable != null)
- {
- var spell = fileService.SpellTable.GetById(spellId);
- if (spell != null)
- {
- // Use reflection to access the internal Spell_Class object
- // DECAL's Spell wrapper has an internal m_pSpell field that contains the actual data
- var spellType = spell.GetType();
-
- // Try to get the internal spell object first
- var internalSpellField = spellType.GetField("m_pSpell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- if (internalSpellField != null)
- {
- var internalSpell = internalSpellField.GetValue(spell);
- if (internalSpell != null)
- {
- // Now get the icon from the internal spell object
- var internalType = internalSpell.GetType();
-
- // Try icon properties on the internal object
- string[] iconPropertyNames = { "Icon", "icon", "IconId", "iconId", "IconID", "iconID" };
- foreach (var propName in iconPropertyNames)
- {
- try
- {
- // Try as property
- var iconProperty = internalType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconProperty != null)
- {
- var iconValue = iconProperty.GetValue(internalSpell, null);
- if (iconValue is int iconInt && iconInt > 0)
- {
- // Spell icons use raw values (no offset)
- return iconInt;
- }
- else if (iconValue is uint iconUint && iconUint > 0)
- {
- return (int)iconUint;
- }
- }
-
- // Try as field
- var iconField = internalType.GetField(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconField != null)
- {
- var iconValue = iconField.GetValue(internalSpell);
- if (iconValue is int iconInt && iconInt > 0)
- {
- return iconInt;
- }
- else if (iconValue is uint iconUint && iconUint > 0)
- {
- return (int)iconUint;
- }
- }
- }
- catch
- {
- // Continue trying other property names
- }
- }
- }
- }
-
- // Fallback: Try direct properties on the wrapper
- string[] wrapperPropertyNames = { "Icon", "IconId", "IconID" };
- foreach (var propName in wrapperPropertyNames)
- {
- var iconProperty = spellType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
- if (iconProperty != null)
- {
- var iconValue = iconProperty.GetValue(spell, null);
- if (iconValue is int iconId && iconId > 0)
- {
- return iconId;
- }
- }
- }
- }
- }
- }
- catch
- {
- }
-
- // Method 2: Use known spell icon mappings for cantrips
- // These are based on AC spell icon IDs from spell data
- var cantripSpellIcons = new Dictionary
- {
- // Strength cantrips - use Strength Self VIII icon
- [2091] = 1332, // Major Strength
- [4325] = 1332, // Epic Strength
- [6107] = 1332, // Legendary Strength
-
- // Endurance cantrips - use Endurance Self VIII icon
- [2061] = 1354, // Major Endurance
- [4226] = 1354, // Epic Endurance
- [6104] = 1354, // Legendary Endurance
-
- // Coordination cantrips - use Coordination Self VIII icon
- [2059] = 1378, // Major Coordination
- [4296] = 1378, // Epic Coordination
- [6102] = 1378, // Legendary Coordination
-
- // Quickness cantrips - use Quickness Self VIII icon
- [2081] = 1409, // Major Quickness
- [4319] = 1409, // Epic Quickness
- [6106] = 1409, // Legendary Quickness
-
- // Focus cantrips - use Focus Self VIII icon
- [2067] = 1426, // Major Focus
- [4304] = 1426, // Epic Focus
- [6105] = 1426, // Legendary Focus
-
- // Willpower/Self cantrips - use Willpower Self VIII icon
- [2091] = 1450, // Major Willpower
- [4329] = 1450, // Epic Willpower
- [6101] = 1450, // Legendary Willpower
-
- // Protection Auras - Armor - use Armor Self VIII icon
- [2113] = 1316, // Major Armor
- [4291] = 1316, // Epic Armor
- [6095] = 1316, // Legendary Armor
-
- // Protection Auras - Physical
- [2245] = 1023, // Major Piercing Ward - use Blade Protection Self VIII
- [4306] = 1023, // Epic Piercing Ward
- [6096] = 1023, // Legendary Piercing Ward
-
- [2244] = 1114, // Major Slashing Ward - use Piercing Protection Self VIII
- [4321] = 1114, // Epic Slashing Ward
- [6097] = 1114, // Legendary Slashing Ward
-
- [2243] = 1138, // Major Bludgeoning Ward - use Bludgeoning Protection Self VIII
- [4293] = 1138, // Epic Bludgeoning Ward
- [6098] = 1138, // Legendary Bludgeoning Ward
-
- // Protection Auras - Elemental
- [2157] = 1096, // Major Frost Ward - use Cold Protection Self VIII
- [4309] = 1096, // Epic Frost Ward
- [6100] = 1096, // Legendary Frost Ward
-
- [2158] = 1035, // Major Flame Ward - use Fire Protection Self VIII
- [4294] = 1035, // Epic Flame Ward
- [6099] = 1035, // Legendary Flame Ward
-
- [2149] = 1078, // Major Acid Ward - use Acid Protection Self VIII
- [4290] = 1078, // Epic Acid Ward
- [6094] = 1078, // Legendary Acid Ward
-
- [2159] = 1161, // Major Storm Ward - use Lightning Protection Self VIII
- [4322] = 1161, // Epic Storm Ward
- [6079] = 1161, // Legendary Storm Ward
-
- // Magic Defense cantrips - use Magic Resistance Self VIII icon
- [2249] = 610, // Major Magic Resistance
- [4314] = 610, // Epic Magic Resistance
- [6067] = 610, // Legendary Magic Resistance
-
- // Melee Defense cantrips - use Invulnerability Self VIII icon
- [2248] = 562, // Major Invulnerability
- [4312] = 562, // Epic Invulnerability
- [6051] = 562, // Legendary Invulnerability
-
- // Missile Defense cantrips - use Impregnability Self VIII icon
- [2247] = 1562, // Major Impregnability
- [4311] = 1562, // Epic Impregnability
- [6055] = 1562, // Legendary Impregnability
-
- // Life Magic cantrips - use Life Magic Mastery Self VIII icon
- [2156] = 610, // Major Life Magic Aptitude
- [4700] = 610, // Epic Life Magic Aptitude
- [6044] = 610, // Legendary Life Magic Aptitude
-
- // War Magic cantrips - use War Magic Mastery Self VIII icon
- [2183] = 634, // Major War Magic Aptitude
- [4715] = 634, // Epic War Magic Aptitude
- [6075] = 634, // Legendary War Magic Aptitude
-
- // Creature Enchantment cantrips - use Creature Enchantment Mastery Self VIII icon
- [2215] = 586, // Major Creature Enchantment Aptitude
- [4689] = 586, // Epic Creature Enchantment Aptitude
- [6042] = 586, // Legendary Creature Enchantment Aptitude
-
- // Item Enchantment cantrips - use Item Enchantment Mastery Self VIII icon
- [2249] = 658, // Major Item Enchantment Aptitude
- [4697] = 658, // Epic Item Enchantment Aptitude
- [6043] = 658, // Legendary Item Enchantment Aptitude
-
- // Void Magic cantrips - use Void Magic Mastery Self VI icon (no VIII version)
- [5427] = 5418, // Major Void Magic Aptitude
- [5428] = 5418, // Epic Void Magic Aptitude
- [5429] = 5418, // Legendary Void Magic Aptitude
-
- // Weapon cantrips
- [2223] = 522, // Major Heavy Weapon Aptitude - use Heavy Weapon Mastery Self VIII
- [4624] = 522, // Epic Heavy Weapon Aptitude
- [6073] = 522, // Legendary Heavy Weapon Aptitude
-
- [2226] = 327, // Major Light Weapon Aptitude - use Light Weapon Mastery Self VI
- [4639] = 327, // Epic Light Weapon Aptitude
- [6074] = 327, // Legendary Light Weapon Aptitude
-
- [2227] = 350, // Major Finesse Weapon Aptitude - use Finesse Weapon Mastery Self VI
- [4638] = 350, // Epic Finesse Weapon Aptitude
- [6072] = 350, // Legendary Finesse Weapon Aptitude
-
- [2230] = 473, // Major Missile Weapon Aptitude - use Missile Weapon Mastery Self VI
- [4713] = 473, // Epic Missile Weapon Aptitude
- [6071] = 473, // Legendary Missile Weapon Aptitude
-
- // Mana Conversion cantrips - use Mana Conversion Mastery Self VIII icon
- [2152] = 658, // Major Mana Conversion Prowess
- [4705] = 658, // Epic Mana Conversion Prowess
- [6048] = 658, // Legendary Mana Conversion Prowess
- };
-
- if (cantripSpellIcons.ContainsKey(spellId))
- {
- int iconId = cantripSpellIcons[spellId];
- // Add offset for spell icons to display correctly in VVS
- // Based on MagTools pattern, spell icons need the offset for display
- int finalIconId = iconId + 0x6000000;
- return finalIconId;
- }
-
- return 0; // No real icon found
- }
- catch
- {
- return 0;
- }
- }
-
- public void RefreshSocietyQuests()
- {
- try
- {
- // TODO: Implement society quest refresh
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing society quests: {ex.Message}");
- }
- }
-
- public void RefreshFacilityHubQuests()
- {
- try
- {
- // TODO: Implement facility hub quest refresh
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing facility hub quests: {ex.Message}");
- }
- }
-
- public void RefreshCharacterFlags()
- {
- try
- {
- // TODO: Implement character flag refresh
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing character flags: {ex.Message}");
- }
- }
-
- public void RefreshCantrips()
- {
- try
- {
-
- if (CoreManager.Current?.CharacterFilter?.Name == null)
- {
- return;
- }
-
- var characterFilter = CoreManager.Current.CharacterFilter;
- var playerObject = CoreManager.Current.WorldFilter[characterFilter.Id];
-
- if (playerObject == null)
- {
- return;
- }
-
-
- // Clear dynamic skill lists
- Cantrips["Specialized Skills"].Clear();
- Cantrips["Trained Skills"].Clear();
-
- // Populate skills dynamically based on character's actual training
- PopulateCharacterSkills(characterFilter);
-
- // Reset all cantrips to "N/A"
- foreach (var category in Cantrips)
- {
- foreach (var cantrip in category.Value.Values)
- {
- cantrip.Value = "N/A";
- cantrip.Color = System.Drawing.Color.White;
- }
- }
-
- // Scan active spells for cantrips using CharacterFilter.Enchantments
- var enchantments = characterFilter.Enchantments;
- if (enchantments != null)
- {
- for (int i = 0; i < enchantments.Count; i++)
- {
- var ench = enchantments[i];
- var spell = SpellManager.GetSpell(ench.SpellId);
- if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None)
- {
- DetectCantrip(ench.SpellId);
- }
- }
- }
- else
- {
- }
-
- // Compute final icon IDs for all cantrips after refresh
- foreach (var category in Cantrips)
- {
- foreach (var cantrip in category.Value.Values)
- {
- cantrip.ComputedIconId = ComputeCantripIcon(cantrip);
- }
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing cantrips: {ex.Message}");
- }
- }
-
- private void PopulateCharacterSkills(CharacterFilter characterFilter)
- {
- try
- {
- // Map of skill IDs to skill names - based on DECAL CharFilterSkillType enumeration
- // Reference: DECAL API documentation and AC development sources
- var skillIdToName = new Dictionary
- {
- [1] = "Axe", // Retired weapon skill
- [2] = "Bow", // Retired weapon skill
- [3] = "Crossbow", // Retired weapon skill
- [4] = "Dagger", // Retired weapon skill
- [5] = "Mace", // Retired weapon skill
- [6] = "Melee Defense", // Active defense skill
- [7] = "Missile Defense", // Active defense skill
- [8] = "Sling", // Retired weapon skill
- [9] = "Spear", // Retired weapon skill
- [10] = "Staff", // Retired weapon skill
- [11] = "Sword", // Retired weapon skill
- [12] = "Thrown Weapons", // Retired weapon skill
- [13] = "Unarmed Combat", // Retired weapon skill
- [14] = "Arcane Lore", // Active magic skill
- [15] = "Magic Defense", // Active defense skill
- [16] = "Mana Conversion", // Active magic skill
- [17] = "Spellcraft", // Unused/Reserved
- [18] = "Item Tinkering", // Active tinker skill
- [19] = "Assess Person", // Active misc skill
- [20] = "Deception", // Active misc skill
- [21] = "Healing", // Active misc skill
- [22] = "Jump", // Active misc skill
- [23] = "Lockpick", // Active misc skill
- [24] = "Run", // Active misc skill
- [25] = "Awareness", // Unused/Reserved
- [26] = "Arms and Armor Repair", // Unused/Reserved
- [27] = "Assess Creature", // Active misc skill
- [28] = "Weapon Tinkering", // Active tinker skill
- [29] = "Armor Tinkering", // Active tinker skill
- [30] = "Magic Item Tinkering", // Active tinker skill
- [31] = "Creature Enchantment", // Active magic skill
- [32] = "Item Enchantment", // Active magic skill
- [33] = "Life Magic", // Active magic skill
- [34] = "War Magic", // Active magic skill
- [35] = "Leadership", // Active misc skill
- [36] = "Loyalty", // Active misc skill
- [37] = "Fletching", // Active tinker skill
- [38] = "Alchemy", // Active tinker skill
- [39] = "Cooking", // Active tinker skill
- [40] = "Salvaging", // Active tinker skill
- [41] = "Two Handed Combat", // Active weapon skill
- [42] = "Gearcraft", // Retired tinker skill
- [43] = "Void Magic", // Active magic skill
- [44] = "Heavy Weapons", // Active weapon skill
- [45] = "Light Weapons", // Active weapon skill
- [46] = "Finesse Weapons", // Active weapon skill
- [47] = "Missile Weapons", // Active weapon skill
- [48] = "Shield", // Active weapon skill
- [49] = "Dual Wield", // Active weapon skill
- [50] = "Recklessness", // Active weapon skill
- [51] = "Sneak Attack", // Active weapon skill
- [52] = "Dirty Fighting", // Active weapon skill
- [53] = "Threat Assessment", // Unused/Reserved
- [54] = "Summoning" // Active magic skill
- };
-
- // Check each skill's training status
- foreach (var kvp in skillIdToName)
- {
- int skillId = kvp.Key;
- string skillName = kvp.Value;
-
- try
- {
- // Get skill training status using CharacterFilter.Skills
- var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId];
- if (skillInfo == null) continue;
-
- // Apply skill name replacements for cantrips
- if (SkillCantripReplacements.ContainsKey(skillId))
- {
- skillName = SkillCantripReplacements[skillId];
- }
-
- if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized)
- {
- Cantrips["Specialized Skills"][skillName] = new CantripInfo
- {
- Name = skillName,
- Value = "N/A",
- Color = System.Drawing.Color.White
- // IconId removed - will be set by spell icon when cantrips are detected
- };
- }
- else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained)
- {
- Cantrips["Trained Skills"][skillName] = new CantripInfo
- {
- Name = skillName,
- Value = "N/A",
- Color = System.Drawing.Color.White
- // IconId removed - will be set by spell icon when cantrips are detected
- };
- }
- }
- catch
- {
- // Skill not available on this character, skip it
- }
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error populating character skills: {ex.Message}");
- }
- }
-
- private int? GetSkillIconId(int skillId)
- {
- try
- {
- var characterFilter = CoreManager.Current.CharacterFilter;
- if (characterFilter == null)
- {
- return GetFallbackSkillIcon(skillId);
- }
-
- // Validate skillId range for DECAL API
- if (skillId < 1 || skillId > 54)
- {
- return GetFallbackSkillIcon(skillId);
- }
-
- try
- {
- var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId];
- if (skillInfo == null)
- {
- return GetFallbackSkillIcon(skillId);
- }
-
-
- // Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property)
- var skillType = skillInfo.GetType();
-
- // Method 1: Try FileService SkillTable approach (most reliable)
- int realIconId = GetRealSkillIconFromDat(skillId);
- if (realIconId > 0)
- {
- return realIconId + 0x6000000;
- }
-
- // Method 2: Reflection on SkillInfoWrapper.Dat
- var datProperty = skillType.GetProperty("Dat", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (datProperty != null)
- {
- var datObject = datProperty.GetValue(skillInfo, null);
- if (datObject != null)
- {
- var datType = datObject.GetType();
-
- // Try the exact property names from AC system
- string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" };
-
- foreach (var propName in iconPropertyNames)
- {
- var iconProperty = datType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconProperty != null)
- {
- var iconValue = iconProperty.GetValue(datObject, null);
- if (iconValue != null)
- {
- if (iconValue is int iconId && iconId > 0)
- {
- return iconId + 0x6000000;
- }
- else if (iconValue is uint uiconId && uiconId > 0)
- {
- return (int)uiconId + 0x6000000;
- }
- }
- }
- else
- {
- // Try as field
- var iconField = datType.GetField(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconField != null)
- {
- var iconValue = iconField.GetValue(datObject);
- if (iconValue != null)
- {
- if (iconValue is int iconId && iconId > 0)
- {
- return iconId + 0x6000000;
- }
- else if (iconValue is uint uiconId && uiconId > 0)
- {
- return (int)uiconId + 0x6000000;
- }
- }
- }
- }
- }
-
- foreach (var prop in datType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
- {
- try
- {
- var val = prop.GetValue(datObject, null);
- PluginCore.WriteToChat($" {prop.Name}: {val} ({prop.PropertyType.Name})");
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($" {prop.Name}: ({prop.PropertyType.Name})");
- }
- }
- }
- else
- {
- }
- }
- else
- {
- }
-
- // Method 3: Try direct properties on SkillInfoWrapper
- string[] directPropertyNames = { "IconID", "Icon", "IconId", "GraphicID" };
- foreach (var propName in directPropertyNames)
- {
- var iconProperty = skillType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconProperty != null)
- {
- var iconValue = iconProperty.GetValue(skillInfo, null);
- if (iconValue is int iconId && iconId > 0)
- {
- return iconId + 0x6000000;
- }
- }
- }
- }
- catch
- {
- }
-
- // Fallback to predefined mapping
- return GetFallbackSkillIcon(skillId);
- }
- catch
- {
- return GetFallbackSkillIcon(skillId);
- }
- }
-
- private string GetSkillName(int skillId)
- {
- var skillNames = new Dictionary
- {
- [1] = "Axe", [2] = "Bow", [3] = "Crossbow", [4] = "Dagger", [5] = "Mace",
- [6] = "Melee Defense", [7] = "Missile Defense", [8] = "Sling", [9] = "Spear", [10] = "Staff",
- [11] = "Sword", [12] = "Thrown Weapons", [13] = "Unarmed Combat", [14] = "Arcane Lore", [15] = "Magic Defense",
- [16] = "Mana Conversion", [17] = "Spellcraft", [18] = "Item Tinkering", [19] = "Assess Person", [20] = "Deception",
- [21] = "Healing", [22] = "Jump", [23] = "Lockpick", [24] = "Run", [25] = "Awareness",
- [26] = "Arms and Armor Repair", [27] = "Assess Creature", [28] = "Weapon Tinkering", [29] = "Armor Tinkering", [30] = "Magic Item Tinkering",
- [31] = "Creature Enchantment", [32] = "Item Enchantment", [33] = "Life Magic", [34] = "War Magic", [35] = "Leadership",
- [36] = "Loyalty", [37] = "Fletching", [38] = "Alchemy", [39] = "Cooking", [40] = "Salvaging",
- [41] = "Two Handed Combat", [42] = "Gearcraft", [43] = "Void Magic", [44] = "Heavy Weapons", [45] = "Light Weapons",
- [46] = "Finesse Weapons", [47] = "Missile Weapons", [48] = "Shield", [49] = "Dual Wield", [50] = "Recklessness",
- [51] = "Sneak Attack", [52] = "Dirty Fighting", [53] = "Threat Assessment", [54] = "Summoning"
- };
-
- return skillNames.ContainsKey(skillId) ? skillNames[skillId] : $"Unknown({skillId})";
- }
-
- private int GetRealSkillIconFromDat(int skillId)
- {
- try
- {
- // Try using FileService SkillTable directly (similar to CharacterCreation.cs pattern)
- var fileService = CoreManager.Current.Filter();
- if (fileService?.SkillTable != null)
- {
- // Try to get skill data from the skill table
- try
- {
- // Access SkillTable via reflection to get skill data
- var skillTableType = fileService.SkillTable.GetType();
-
- // Look for methods that can get skill by ID
- var methods = skillTableType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- foreach (var method in methods)
- {
- if (method.Name.Contains("Get") || method.Name.Contains("get") || method.Name == "Item")
- {
- var parameters = method.GetParameters();
- if (parameters.Length == 1 && (parameters[0].ParameterType == typeof(int) || parameters[0].ParameterType == typeof(uint)))
- {
- try
- {
- var skillData = method.Invoke(fileService.SkillTable, new object[] { skillId });
- if (skillData != null)
- {
-
- // Look for icon properties on the skill data
- var skillDataType = skillData.GetType();
- string[] iconProps = { "IconID", "Icon", "IconId", "GraphicID", "uiGraphic" };
-
- foreach (var propName in iconProps)
- {
- var iconProp = skillDataType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- if (iconProp != null)
- {
- var iconValue = iconProp.GetValue(skillData, null);
- if (iconValue is int iconInt && iconInt > 0)
- {
- return iconInt;
- }
- else if (iconValue is uint iconUint && iconUint > 0)
- {
- return (int)iconUint;
- }
- }
- }
- }
- }
- catch
- {
- // Method call failed, try next one
- }
- }
- }
- }
- }
- catch
- {
- }
- }
- else
- {
- }
-
- return 0; // No icon found
- }
- catch
- {
- return 0;
- }
- }
-
- private int? GetFallbackSkillIcon(int skillId)
- {
- // Use proven working icon IDs from the recall spells system
- // These icons are confirmed to display correctly in VVS
- var skillIconMap = new Dictionary
- {
- // Magic Skills - Use magical/mystical icons (from recalls system)
- [14] = 0x6002D14, // Arcane Lore - Portal icon (default recall icon)
- [16] = 0x60011F9, // Mana Conversion - Green circle (confirmed working)
- [31] = 0x6002D14, // Creature Enchantment - Portal icon
- [32] = 0x6002D14, // Item Enchantment - Portal icon
- [33] = 0x60011F9, // Life Magic - Green circle (life/healing)
- [34] = 0x60011F8, // War Magic - Red circle (destruction)
- [43] = 0x600287A, // Void Magic - Gray dot (void)
- [54] = 0x6002D14, // Summoning - Portal icon
-
- // Combat Skills - Use distinct working icons
- [41] = 0x60011F8, // Two Handed Combat - Red circle
- [44] = 0x60011F8, // Heavy Weapons - Red circle
- [45] = 0x60028FC, // Light Weapons - Up arrow (from recalls)
- [46] = 0x60028FD, // Finesse Weapons - Down arrow (from recalls)
- [47] = 0x60020B5, // Missile Weapons - Question mark (from recalls)
- [48] = 0x600287A, // Shield - Gray dot
- [49] = 0x60011F8, // Dual Wield - Red circle
- [50] = 0x60011F8, // Recklessness - Red circle
- [51] = 0x600287A, // Sneak Attack - Gray dot
- [52] = 0x60011F8, // Dirty Fighting - Red circle
-
- // Defense Skills - Use defensive icons
- [6] = 0x600287A, // Melee Defense - Gray dot
- [7] = 0x600287A, // Missile Defense - Gray dot
- [15] = 0x600287A, // Magic Defense - Gray dot
-
- // Misc Skills - Use varied working icons
- [19] = 0x60020B5, // Assess Person - Question mark (from recalls)
- [20] = 0x60020B5, // Deception - Question mark
- [21] = 0x60011F9, // Healing - Green circle
- [22] = 0x60028FC, // Jump - Up arrow (motion)
- [23] = 0x60020B5, // Lockpick - Question mark
- [24] = 0x60028FC, // Run - Up arrow (motion)
- [27] = 0x60020B5, // Assess Creature - Question mark
- [35] = 0x60020B5, // Leadership - Question mark
- [36] = 0x60020B5, // Loyalty - Question mark
-
- // Craft Skills - Use working craft icons
- [18] = 0x600287A, // Item Tinkering - Gray dot
- [28] = 0x600287A, // Weapon Tinkering - Gray dot
- [29] = 0x600287A, // Armor Tinkering - Gray dot
- [30] = 0x600287A, // Magic Item Tinkering - Gray dot
- [37] = 0x600287A, // Fletching - Gray dot
- [38] = 0x600287A, // Alchemy - Gray dot
- [39] = 0x600287A, // Cooking - Gray dot
- [40] = 0x600287A, // Salvaging - Gray dot
-
- // Retired weapon skills - Use weapon-style icons
- [1] = 0x60011F8, // Axe - Red circle
- [2] = 0x60028FC, // Bow - Up arrow (projectile)
- [3] = 0x60028FC, // Crossbow - Up arrow (projectile)
- [4] = 0x60011F8, // Dagger - Red circle
- [5] = 0x60011F8, // Mace - Red circle
- [8] = 0x60028FC, // Sling - Up arrow (projectile)
- [9] = 0x60011F8, // Spear - Red circle
- [10] = 0x60011F8, // Staff - Red circle
- [11] = 0x60011F8, // Sword - Red circle
- [12] = 0x60028FC, // Thrown Weapons - Up arrow (projectile)
- [13] = 0x60011F8 // Unarmed Combat - Red circle
- };
-
- if (skillIconMap.ContainsKey(skillId))
- {
- return skillIconMap[skillId];
- }
-
- // Final fallback to proven working icon from recalls system
- return 0x6002D14; // Portal icon - confirmed working in recalls
- }
-
- private int ComputeCantripIcon(CantripInfo cantrip)
- {
- try
- {
- // Green circle for active cantrips, red circle for missing cantrips
- if (cantrip.Value != "N/A")
- {
- return 0x60011F9; // Green circle - has cantrip
- }
- else
- {
- return 0x60011F8; // Red circle - missing cantrip
- }
- }
- catch
- {
- return 0x60011F8; // Red circle on error
- }
- }
-
- private void DetectCantrip(int spellId)
- {
- try
- {
- // Get spell name from SpellManager
- string spellName = GetSpellName(spellId);
- if (string.IsNullOrEmpty(spellName))
- {
- return;
- }
-
- // Debug output to see what spells we're processing
-
- // Define cantrip levels and their patterns
- var cantripPatterns = new Dictionary
- {
- ["Minor"] = ("Minor", System.Drawing.Color.White),
- ["Moderate"] = ("Moderate", System.Drawing.Color.Green),
- ["Major"] = ("Major", System.Drawing.Color.Blue),
- ["Epic"] = ("Epic", System.Drawing.Color.Purple),
- ["Legendary"] = ("Legendary", System.Drawing.Color.Orange)
- };
-
- // Check each cantrip level
- foreach (var cantripPattern in cantripPatterns)
- {
- string pattern = cantripPattern.Key;
- var (level, color) = cantripPattern.Value;
-
- if (!spellName.StartsWith(pattern + " ")) continue;
-
- // Remove the level prefix to get the skill/attribute name
- string skillPart = spellName.Substring(pattern.Length + 1);
-
- // Get the spell icon for this cantrip spell
- int spellIconId = GetRealSpellIcon(spellId);
- if (spellIconId == 0)
- {
- spellIconId = 0x6002D14; // Default fallback icon
- }
- else
- {
- }
-
- // Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward")
- if (MatchProtectionAura(skillPart, level, color, spellIconId))
- {
- return;
- }
-
- // Try to match Attributes (exact format: "Minor Strength", "Epic Focus")
- if (MatchAttribute(skillPart, level, color, spellIconId))
- {
- return;
- }
-
- // Try to match Skills using the replacement mappings
- if (MatchSkill(skillPart, level, color, spellIconId))
- {
- return;
- }
-
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error detecting cantrip for spell {spellId}: {ex.Message}");
- }
- }
-
- private bool MatchProtectionAura(string skillPart, string level, System.Drawing.Color color, int spellIconId)
- {
- // Map AC cantrip spell names to protection aura names
- var protectionMappings = new Dictionary
- {
- ["Armor"] = "Armor",
- ["Bludgeoning Ward"] = "Bludgeoning Ward",
- ["Piercing Ward"] = "Piercing Ward",
- ["Slashing Ward"] = "Slashing Ward",
- ["Flame Ward"] = "Flame Ward",
- ["Frost Ward"] = "Frost Ward",
- ["Cold Ward"] = "Frost Ward", // AC also uses "Cold Ward"
- ["Acid Ward"] = "Acid Ward",
- ["Storm Ward"] = "Storm Ward", // AC spell is "Storm Ward"
- ["Lightning Ward"] = "Storm Ward", // AC also uses "Lightning Ward"
-
- // Add more variations that might appear in AC spell names
- ["Bludgeoning Protection"] = "Bludgeoning Ward",
- ["Piercing Protection"] = "Piercing Ward",
- ["Slashing Protection"] = "Slashing Ward",
- ["Fire Protection"] = "Flame Ward",
- ["Cold Protection"] = "Frost Ward",
- ["Acid Protection"] = "Acid Ward",
- ["Lightning Protection"] = "Storm Ward",
-
- // Single word variations
- ["Bludgeoning"] = "Bludgeoning Ward",
- ["Piercing"] = "Piercing Ward",
- ["Slashing"] = "Slashing Ward",
- ["Fire"] = "Flame Ward",
- ["Flame"] = "Flame Ward",
- ["Cold"] = "Frost Ward",
- ["Frost"] = "Frost Ward",
- ["Acid"] = "Acid Ward",
- ["Lightning"] = "Storm Ward",
- ["Storm"] = "Storm Ward"
- };
-
- foreach (var mapping in protectionMappings)
- {
- if (skillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase))
- {
- // Create the cantrip entry if it doesn't exist
- if (!Cantrips["Protection Auras"].ContainsKey(mapping.Value))
- {
- Cantrips["Protection Auras"][mapping.Value] = new CantripInfo
- {
- Name = mapping.Value,
- Value = "N/A",
- Color = System.Drawing.Color.White
- };
- }
-
- var cantrip = Cantrips["Protection Auras"][mapping.Value];
- if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
- {
- cantrip.Value = level;
- cantrip.Color = color;
- cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
- }
- return true;
- }
- }
- return false;
- }
-
- private bool MatchAttribute(string skillPart, string level, System.Drawing.Color color, int spellIconId)
- {
- // Clean the skill part - remove extra spaces and normalize
- string cleanedSkillPart = skillPart.Trim();
-
- var attributeMappings = new Dictionary
- {
- ["Strength"] = "Strength",
- ["Endurance"] = "Endurance",
- ["Coordination"] = "Coordination",
- ["Quickness"] = "Quickness",
- ["Focus"] = "Focus",
- ["Self"] = "Willpower", // "Minor Self" -> Willpower
- ["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower
- };
-
-
- foreach (var mapping in attributeMappings)
- {
- if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase))
- {
-
- // Create the cantrip entry if it doesn't exist
- if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
- {
- Cantrips["Attributes"][mapping.Value] = new CantripInfo
- {
- Name = mapping.Value,
- Value = "N/A",
- Color = System.Drawing.Color.White
- };
- }
-
- var cantrip = Cantrips["Attributes"][mapping.Value];
- if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
- {
- cantrip.Value = level;
- cantrip.Color = color;
- cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
- }
- return true;
- }
- }
-
- // Try more flexible matching - check if the cleaned skill part contains any of our attributes
- foreach (var mapping in attributeMappings)
- {
- if (cleanedSkillPart.IndexOf(mapping.Key, StringComparison.OrdinalIgnoreCase) >= 0)
- {
-
- // Create the cantrip entry if it doesn't exist
- if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
- {
- Cantrips["Attributes"][mapping.Value] = new CantripInfo
- {
- Name = mapping.Value,
- Value = "N/A",
- Color = System.Drawing.Color.White
- };
- }
-
- var cantrip = Cantrips["Attributes"][mapping.Value];
- if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
- {
- cantrip.Value = level;
- cantrip.Color = color;
- cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
- }
- return true;
- }
- }
-
- return false;
- }
-
- private bool MatchSkill(string skillPart, string level, System.Drawing.Color color, int spellIconId)
- {
- // Map actual cantrip spell names to our skill names
- var skillMappings = new Dictionary
- {
- // Defense skills (with AC spell name mappings)
- ["Magic Resistance"] = "MagicResistance",
- ["Invulnerability"] = "Invulnerability",
- ["Impregnability"] = "Impgrenability", // Note: AC uses "Impregnability" not "Impgrenability"
-
- // Weapon skills (with AC spell name patterns)
- ["Heavy Weapon Aptitude"] = "HeavyWeapon",
- ["Light Weapon Aptitude"] = "LightWeapon",
- ["Finesse Weapon Aptitude"] = "FinesseWeapon",
- ["Missile Weapon Aptitude"] = "MissileWeapon",
-
- // Craft skills (with AC spell name patterns)
- ["Alchemical Prowess"] = "Alchemy",
- ["Arcane Prowess"] = "Arcane Lore",
- ["Armor Tinkering Expertise"] = "Armor Tinkering",
- ["Assess Creature"] = "Assess Creature",
- ["Assess Person"] = "Assess Person",
- ["Cooking Prowess"] = "Cooking",
- ["Deception Prowess"] = "Deception",
- ["Fletching Prowess"] = "Fletching",
- ["Healing Prowess"] = "Healing",
- ["Item Tinkering Expertise"] = "Item Tinkering",
- ["Leadership"] = "Leadership",
- ["Lockpick Prowess"] = "Lockpick",
- ["Fealty"] = "Loyalty", // AC uses "Fealty" for Loyalty
- ["Magic Item Tinkering Expertise"] = "Magic Item Tinkering",
- ["Mana Conversion Prowess"] = "Mana Conversion",
- ["Jumping Prowess"] = "Jump", // AC has Jump cantrips
- ["Salvaging Aptitude"] = "Salvaging",
- ["Weapon Tinkering Expertise"] = "Weapon Tinkering",
-
- // Magic schools (with AC spell name patterns)
- ["War Magic Aptitude"] = "War Magic",
- ["Life Magic Aptitude"] = "Life Magic",
- ["Creature Enchantment Aptitude"] = "Creature Enchantment",
- ["Item Enchantment Aptitude"] = "Item Enchantment",
- ["Void Magic Aptitude"] = "Void Magic",
- ["Summoning Prowess"] = "Summoning",
-
- // Combat skills
- ["Two Handed Combat Aptitude"] = "Two Handed Combat",
- ["Dual Wield Aptitude"] = "Dual Wield",
- ["Shield Aptitude"] = "Shield",
- ["Sneak Attack Prowess"] = "Sneak Attack",
- ["Dirty Fighting Prowess"] = "Dirty Fighting",
- ["Recklessness"] = "Recklessness"
- };
-
- foreach (var mapping in skillMappings)
- {
- if (skillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase))
- {
- // Check both specialized and trained skills
- foreach (var category in new[] { "Specialized Skills", "Trained Skills" })
- {
- if (Cantrips[category].ContainsKey(mapping.Value))
- {
- var cantrip = Cantrips[category][mapping.Value];
- if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
- {
- cantrip.Value = level;
- cantrip.Color = color;
- cantrip.SpellIconId = spellIconId; // Use the actual spell icon instead of skill icon
- cantrip.IconId = null; // Clear any skill icon reference
- }
- return true;
- }
- }
- }
- }
- return false;
- }
-
- private string GetSpellName(int spellId)
- {
- try
- {
- // Use our existing SpellManager that was already working
- var spell = SpellManager.GetSpell(spellId);
- if (spell != null)
- {
- return spell.Name;
- }
- return "";
- }
- catch
- {
- return "";
- }
- }
-
- private bool IsHigherCantripLevel(string newLevel, string currentLevel)
- {
- var levels = new Dictionary
- {
- ["Minor"] = 1,
- ["Moderate"] = 2,
- ["Major"] = 3,
- ["Epic"] = 4,
- ["Legendary"] = 5
- };
-
- if (!levels.ContainsKey(newLevel) || !levels.ContainsKey(currentLevel))
- return false;
-
- return levels[newLevel] > levels[currentLevel];
- }
-
- private void TestCantripDetection()
- {
- try
- {
- // Test real AC cantrip spell names to verify the detection logic
- var testSpells = new[]
- {
- "Major Strength", // Attribute cantrip
- "Epic Coordination", // Attribute cantrip
- "Legendary Focus", // Attribute cantrip
- "Major Willpower", // Willpower attribute
- "Major Invulnerability", // Melee Defense skill
- "Major Impregnability", // Missile Defense skill
- "Major Alchemical Prowess", // Alchemy skill
- "Major Arcane Prowess", // Arcane Lore skill
- "Epic Life Magic Aptitude", // Life Magic skill
- "Major Fealty", // Loyalty skill
- "Major Armor", // Protection aura
- "Epic Flame Ward", // Protection aura
- "Legendary Storm Ward" // Protection aura
- };
-
- foreach (var spellName in testSpells)
- {
- TestDetectCantripByName(spellName);
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error in test cantrip detection: {ex.Message}");
- }
- }
-
- private void TestDetectCantripByName(string spellName)
- {
- try
- {
- // Simulate the detection logic with a fake spell name
-
- // Define cantrip levels and their patterns
- var cantripPatterns = new Dictionary
- {
- ["Minor"] = ("Minor", System.Drawing.Color.White),
- ["Moderate"] = ("Moderate", System.Drawing.Color.Green),
- ["Major"] = ("Major", System.Drawing.Color.Blue),
- ["Epic"] = ("Epic", System.Drawing.Color.Purple),
- ["Legendary"] = ("Legendary", System.Drawing.Color.Orange)
- };
-
- // Check each cantrip level
- foreach (var cantripPattern in cantripPatterns)
- {
- string pattern = cantripPattern.Key;
- var (level, color) = cantripPattern.Value;
-
- if (!spellName.StartsWith(pattern + " ")) continue;
-
- // Remove the level prefix to get the skill/attribute name
- string skillPart = spellName.Substring(pattern.Length + 1);
-
-
- // Get a test spell icon (use default for testing)
- int testSpellIconId = 0x6002D14;
-
- // Try to match Protection Auras first
- if (MatchProtectionAura(skillPart, level, color, testSpellIconId))
- {
- return;
- }
-
- // Try to match Attributes
- if (MatchAttribute(skillPart, level, color, testSpellIconId))
- {
- return;
- }
-
- // Try to match Skills
- if (MatchSkill(skillPart, level, color, testSpellIconId))
- {
- return;
- }
-
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error testing cantrip detection: {ex.Message}");
- }
- }
-
- #region Weapon Data Structures
- public class TrackedWeaponInfo
- {
- public string Name { get; set; }
- public string Category { get; set; }
- public string WeaponType { get; set; }
- public string Status { get; set; }
- public bool IsAcquired { get; set; }
- public int ItemId { get; set; }
- public int IconId { get; set; }
- }
-
- public Dictionary> WeaponCategories { get; private set; }
- #endregion
-
- public void RefreshWeapons()
- {
- try
- {
- if (CoreManager.Current?.CharacterFilter?.Name == null) return;
-
- InitializeNewWeaponData();
- CheckAcquiredWeapons();
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error refreshing weapons: {ex.Message}");
- }
- }
-
- private void InitializeNewWeaponData()
- {
- WeaponCategories = new Dictionary>
- {
- ["Legendary Weapons"] = new List
- {
- new TrackedWeaponInfo { Name = "Bow of the Quiddity", Category = "Legendary Weapons", WeaponType = "Bow", ItemId = 23044, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Sword of the Quiddity", Category = "Legendary Weapons", WeaponType = "Sword", ItemId = 23039, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Mace of the Quiddity", Category = "Legendary Weapons", WeaponType = "Mace", ItemId = 23040, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlatl of the Quiddity", Category = "Legendary Weapons", WeaponType = "Atlatl", ItemId = 23043, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Staff of the Quiddity", Category = "Legendary Weapons", WeaponType = "Staff", ItemId = 23041, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Dagger of the Quiddity", Category = "Legendary Weapons", WeaponType = "Dagger", ItemId = 23042, Status = "Unknown" }
- },
- ["Slayer Weapons"] = new List
- {
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Bow", Category = "Slayer Weapons", WeaponType = "Bow", ItemId = 20636, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Sword", Category = "Slayer Weapons", WeaponType = "Sword", ItemId = 20631, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Staff", Category = "Slayer Weapons", WeaponType = "Staff", ItemId = 20633, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Dagger", Category = "Slayer Weapons", WeaponType = "Dagger", ItemId = 20634, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Axe", Category = "Slayer Weapons", WeaponType = "Axe", ItemId = 20635, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Shadowfire Isparian Mace", Category = "Slayer Weapons", WeaponType = "Mace", ItemId = 20632, Status = "Unknown" }
- },
- ["Society Weapons"] = new List
- {
- new TrackedWeaponInfo { Name = "Radiant Blood Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32834, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Celestial Hand Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32835, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Eldritch Web Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32836, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Radiant Blood Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32837, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Celestial Hand Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32838, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Eldritch Web Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32839, Status = "Unknown" }
- },
- ["Atlan Weapons"] = new List
- {
- new TrackedWeaponInfo { Name = "Atlan Sword", Category = "Atlan Weapons", WeaponType = "Sword", ItemId = 11648, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlan Axe", Category = "Atlan Weapons", WeaponType = "Axe", ItemId = 11649, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlan Mace", Category = "Atlan Weapons", WeaponType = "Mace", ItemId = 11650, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlan Spear", Category = "Atlan Weapons", WeaponType = "Spear", ItemId = 11651, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlan Staff", Category = "Atlan Weapons", WeaponType = "Staff", ItemId = 11652, Status = "Unknown" },
- new TrackedWeaponInfo { Name = "Atlan Dagger", Category = "Atlan Weapons", WeaponType = "Dagger", ItemId = 11653, Status = "Unknown" }
- }
- };
- }
-
- private void CheckAcquiredWeapons()
- {
- try
- {
- // Get inventory items
- var worldFilter = CoreManager.Current.WorldFilter;
- var inventoryItems = worldFilter.GetInventory().Cast().ToList();
-
- // Check each weapon category
- foreach (var category in WeaponCategories)
- {
- foreach (var weapon in category.Value)
- {
- // Check if weapon is acquired by name matching
- var foundItem = inventoryItems.FirstOrDefault(item =>
- item.Name.Contains(weapon.Name) ||
- weapon.Name.Contains(item.Name));
-
- if (foundItem != null)
- {
- weapon.IsAcquired = true;
- weapon.Status = "Acquired";
- weapon.IconId = foundItem.Icon + 0x6000000;
- }
- else
- {
- weapon.IsAcquired = false;
- weapon.Status = "Not Acquired";
- weapon.IconId = 0x6002D14; // Default icon
- }
- }
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error checking acquired weapons: {ex.Message}");
- }
- }
- #endregion
-
- #region Cleanup
- public void Dispose()
- {
- // Cleanup if needed
- }
- #endregion
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/FodyWeavers.xml b/MosswartMassacre/FodyWeavers.xml
deleted file mode 100644
index 8c6d208..0000000
--- a/MosswartMassacre/FodyWeavers.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- YamlDotNet
- Newtonsoft.Json
-
-
-
\ No newline at end of file
diff --git a/MosswartMassacre/GameEventRouter.cs b/MosswartMassacre/GameEventRouter.cs
deleted file mode 100644
index 6bda3f9..0000000
--- a/MosswartMassacre/GameEventRouter.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System;
-using Decal.Adapter;
-
-namespace MosswartMassacre
-{
- ///
- /// Routes EchoFilter.ServerDispatch network messages to the appropriate handlers.
- /// Owns the routing of 0xF7B0 sub-events and 0x02CF to CharacterStats.
- ///
- internal class GameEventRouter
- {
- private readonly IPluginLogger _logger;
-
- internal GameEventRouter(IPluginLogger logger)
- {
- _logger = logger;
- }
-
- internal void OnServerDispatch(object sender, NetworkMessageEventArgs e)
- {
- try
- {
- if (e.Message.Type == Constants.GameEventMessageType)
- {
- int eventId = (int)e.Message["event"];
-
- if (eventId == Constants.AllegianceInfoEvent)
- {
- CharacterStats.ProcessAllegianceInfoMessage(e);
- }
- else if (eventId == Constants.LoginCharacterEvent)
- {
- CharacterStats.ProcessCharacterPropertyData(e);
- }
- else if (eventId == Constants.TitlesListEvent)
- {
- CharacterStats.ProcessTitlesMessage(e);
- }
- else if (eventId == Constants.SetTitleEvent)
- {
- CharacterStats.ProcessSetTitleMessage(e);
- }
- }
- else if (e.Message.Type == Constants.PrivateUpdatePropertyInt64)
- {
- CharacterStats.ProcessPropertyInt64Update(e);
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[CharStats] ServerDispatch error: {ex.Message}");
- }
- }
- }
-}
diff --git a/MosswartMassacre/HttpCommandServer.cs b/MosswartMassacre/HttpCommandServer.cs
new file mode 100644
index 0000000..35a6d38
--- /dev/null
+++ b/MosswartMassacre/HttpCommandServer.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Decal.Adapter;
+
+namespace MosswartMassacre
+{
+ public static class HttpCommandServer
+ {
+ private static HttpListener listener;
+ private static CancellationTokenSource cts;
+ private static bool isRunning = false;
+
+ public static bool IsRunning => isRunning;
+
+ public static void Start()
+ {
+ if (isRunning) return;
+
+ try
+ {
+ listener = new HttpListener();
+ listener.Prefixes.Add("http://localhost:8085/");
+ listener.Start();
+ cts = new CancellationTokenSource();
+ Task.Run(() => ListenLoop(cts.Token));
+
+ isRunning = true;
+ PluginCore.WriteToChat("[HTTP] Server started on port 8085.");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("[HTTP] Error starting server: " + ex.Message);
+ }
+ }
+
+ public static void Stop()
+ {
+ if (!isRunning) return;
+
+ try
+ {
+ cts.Cancel();
+ listener.Stop();
+ listener.Close();
+ listener = null;
+ isRunning = false;
+ PluginCore.WriteToChat("[HTTP] Server stopped.");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("[HTTP] Error stopping server: " + ex.Message);
+ }
+ }
+
+ private static async Task ListenLoop(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ HttpListenerContext context = null;
+
+ try
+ {
+ context = await listener.GetContextAsync();
+ }
+ catch (HttpListenerException)
+ {
+ break; // Listener was stopped
+ }
+
+ if (context == null) continue;
+
+ string requestBody = new System.IO.StreamReader(context.Request.InputStream).ReadToEnd();
+
+ PluginCore.WriteToChat("[HTTP] Received request: " + requestBody);
+
+ // Parse simple format: target=Name&command=/say hello
+ string target = "";
+ string command = "";
+ foreach (var pair in requestBody.Split('&'))
+ {
+ var parts = pair.Split('=');
+ if (parts.Length == 2)
+ {
+ if (parts[0] == "target") target = WebUtility.UrlDecode(parts[1]);
+ else if (parts[0] == "command") command = WebUtility.UrlDecode(parts[1]);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(target) && !string.IsNullOrWhiteSpace(command))
+ {
+ string tellCmd = $"/a {target} {command}";
+ CoreManager.Current.Actions.InvokeChatParser(tellCmd);
+ }
+
+ byte[] buffer = Encoding.UTF8.GetBytes("Command received.");
+ context.Response.ContentLength64 = buffer.Length;
+ context.Response.OutputStream.Write(buffer, 0, buffer.Length);
+ context.Response.OutputStream.Close();
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/IGameStats.cs b/MosswartMassacre/IGameStats.cs
deleted file mode 100644
index ba0cb6f..0000000
--- a/MosswartMassacre/IGameStats.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-
-namespace MosswartMassacre
-{
- ///
- /// Provides game statistics for WebSocket telemetry payloads.
- /// Replaces direct static field access on PluginCore.
- ///
- public interface IGameStats
- {
- int TotalKills { get; }
- double KillsPerHour { get; }
- int SessionDeaths { get; }
- int TotalDeaths { get; }
- int CachedPrismaticCount { get; }
- string CharTag { get; }
- DateTime StatsStartTime { get; }
- }
-}
diff --git a/MosswartMassacre/IPluginLogger.cs b/MosswartMassacre/IPluginLogger.cs
deleted file mode 100644
index c9d4157..0000000
--- a/MosswartMassacre/IPluginLogger.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace MosswartMassacre
-{
- ///
- /// Interface for writing messages to the game chat window.
- /// Eliminates direct PluginCore.WriteToChat() dependencies from manager classes.
- ///
- public interface IPluginLogger
- {
- void Log(string message);
- }
-}
diff --git a/MosswartMassacre/InventoryMonitor.cs b/MosswartMassacre/InventoryMonitor.cs
deleted file mode 100644
index 1f60313..0000000
--- a/MosswartMassacre/InventoryMonitor.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-
-namespace MosswartMassacre
-{
- ///
- /// Tracks Prismatic Taper inventory counts using event-driven delta math.
- /// Avoids expensive inventory scans during gameplay.
- ///
- internal class InventoryMonitor
- {
- private readonly IPluginLogger _logger;
- private readonly Dictionary _trackedTaperContainers = new Dictionary();
- private readonly Dictionary _lastKnownStackSizes = new Dictionary();
-
- internal int CachedPrismaticCount { get; private set; }
- internal int LastPrismaticCount { get; private set; }
-
- internal InventoryMonitor(IPluginLogger logger)
- {
- _logger = logger;
- }
-
- internal void Initialize()
- {
- try
- {
- LastPrismaticCount = CachedPrismaticCount;
- CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
-
- _trackedTaperContainers.Clear();
- _lastKnownStackSizes.Clear();
-
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase) &&
- IsPlayerOwnedContainer(wo.Container))
- {
- int stackCount = wo.Values(LongValueKey.StackCount, 1);
- _trackedTaperContainers[wo.Id] = wo.Container;
- _lastKnownStackSizes[wo.Id] = stackCount;
- }
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[TAPER] Error initializing count: {ex.Message}");
- CachedPrismaticCount = 0;
- LastPrismaticCount = 0;
- _trackedTaperContainers.Clear();
- _lastKnownStackSizes.Clear();
- }
- }
-
- internal void OnInventoryCreate(object sender, CreateObjectEventArgs e)
- {
- try
- {
- var item = e.New;
- if (IsPlayerOwnedContainer(item.Container) &&
- item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
- {
- LastPrismaticCount = CachedPrismaticCount;
- int stackCount = item.Values(LongValueKey.StackCount, 1);
- CachedPrismaticCount += stackCount;
-
- _trackedTaperContainers[item.Id] = item.Container;
- _lastKnownStackSizes[item.Id] = stackCount;
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[TAPER] Error in OnInventoryCreate: {ex.Message}");
- }
- }
-
- internal void OnInventoryRelease(object sender, ReleaseObjectEventArgs e)
- {
- try
- {
- var item = e.Released;
- if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
- {
- if (_trackedTaperContainers.TryGetValue(item.Id, out int previousContainer))
- {
- if (IsPlayerOwnedContainer(previousContainer))
- {
- LastPrismaticCount = CachedPrismaticCount;
- int stackCount = item.Values(LongValueKey.StackCount, 1);
- CachedPrismaticCount -= stackCount;
- }
-
- _trackedTaperContainers.Remove(item.Id);
- _lastKnownStackSizes.Remove(item.Id);
- }
- else
- {
- LastPrismaticCount = CachedPrismaticCount;
- CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
- }
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[TAPER] Error in OnInventoryRelease: {ex.Message}");
- }
- }
-
- internal void OnInventoryChange(object sender, ChangeObjectEventArgs e)
- {
- try
- {
- var item = e.Changed;
- if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
- {
- bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container);
-
- if (isInPlayerContainer)
- {
- bool wasAlreadyTracked = _trackedTaperContainers.ContainsKey(item.Id);
- _trackedTaperContainers[item.Id] = item.Container;
-
- int currentStack = item.Values(LongValueKey.StackCount, 1);
-
- if (!wasAlreadyTracked)
- {
- LastPrismaticCount = CachedPrismaticCount;
- CachedPrismaticCount += currentStack;
- }
- else if (_lastKnownStackSizes.TryGetValue(item.Id, out int previousStack))
- {
- int stackDelta = currentStack - previousStack;
- if (stackDelta != 0)
- {
- LastPrismaticCount = CachedPrismaticCount;
- CachedPrismaticCount += stackDelta;
- }
- }
-
- _lastKnownStackSizes[item.Id] = currentStack;
- }
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[TAPER] Error in OnInventoryChange: {ex.Message}");
- }
- }
-
- internal void Cleanup()
- {
- _trackedTaperContainers.Clear();
- _lastKnownStackSizes.Clear();
- }
-
- internal int TrackedTaperCount => _trackedTaperContainers.Count;
- internal int KnownStackSizesCount => _lastKnownStackSizes.Count;
-
- private static bool IsPlayerOwnedContainer(int containerId)
- {
- try
- {
- if (containerId == CoreManager.Current.CharacterFilter.Id)
- return true;
-
- WorldObject container = CoreManager.Current.WorldFilter[containerId];
- if (container != null &&
- container.ObjectClass == ObjectClass.Container &&
- container.Container == CoreManager.Current.CharacterFilter.Id)
- {
- return true;
- }
-
- return false;
- }
- catch
- {
- return false;
- }
- }
- }
-}
diff --git a/MosswartMassacre/KillTracker.cs b/MosswartMassacre/KillTracker.cs
deleted file mode 100644
index 0541f51..0000000
--- a/MosswartMassacre/KillTracker.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using System;
-using System.Text.RegularExpressions;
-using System.Timers;
-
-namespace MosswartMassacre
-{
- ///
- /// Tracks kills, deaths, and kill rate calculations.
- /// Owns the 1-second stats update timer.
- ///
- internal class KillTracker
- {
- private readonly IPluginLogger _logger;
- private readonly Action _onStatsUpdated;
- private readonly Action _onElapsedUpdated;
-
- private int _totalKills;
- private int _sessionDeaths;
- private int _totalDeaths;
- private double _killsPer5Min;
- private double _killsPerHour;
- private DateTime _lastKillTime = DateTime.Now;
- private DateTime _statsStartTime = DateTime.Now;
- private Timer _updateTimer;
-
- // Kill message patterns — all 35+ patterns preserved exactly
- private static readonly string[] KillPatterns = new string[]
- {
- @"^You flatten (?.+)'s body with the force of your assault!$",
- @"^You bring (?.+) to a fiery end!$",
- @"^You beat (?.+) to a lifeless pulp!$",
- @"^You smite (?.+) mightily!$",
- @"^You obliterate (?.+)!$",
- @"^You run (?.+) through!$",
- @"^You reduce (?.+) to a sizzling, oozing mass!$",
- @"^You knock (?.+) into next Morningthaw!$",
- @"^You split (?.+) apart!$",
- @"^You cleave (?.+) in twain!$",
- @"^You slay (?.+) viciously enough to impart death several times over!$",
- @"^You reduce (?.+) to a drained, twisted corpse!$",
- @"^Your killing blow nearly turns (?.+) inside-out!$",
- @"^Your attack stops (?.+) cold!$",
- @"^Your lightning coruscates over (?.+)'s mortal remains!$",
- @"^Your assault sends (?.+) to an icy death!$",
- @"^You killed (?.+)!$",
- @"^The thunder of crushing (?.+) is followed by the deafening silence of death!$",
- @"^The deadly force of your attack is so strong that (?.+)'s ancestors feel it!$",
- @"^(?.+)'s seared corpse smolders before you!$",
- @"^(?.+) is reduced to cinders!$",
- @"^(?.+) is shattered by your assault!$",
- @"^(?.+) catches your attack, with dire consequences!$",
- @"^(?.+) is utterly destroyed by your attack!$",
- @"^(?.+) suffers a frozen fate!$",
- @"^(?.+)'s perforated corpse falls before you!$",
- @"^(?.+) is fatally punctured!$",
- @"^(?.+)'s death is preceded by a sharp, stabbing pain!$",
- @"^(?.+) is torn to ribbons by your assault!$",
- @"^(?.+) is liquified by your attack!$",
- @"^(?.+)'s last strength dissolves before you!$",
- @"^Electricity tears (?.+) apart!$",
- @"^Blistered by lightning, (?.+) falls!$",
- @"^(?.+)'s last strength withers before you!$",
- @"^(?.+) is dessicated by your attack!$",
- @"^(?.+) is incinerated by your assault!$"
- };
-
- internal int TotalKills => _totalKills;
- internal double KillsPerHour => _killsPerHour;
- internal double KillsPer5Min => _killsPer5Min;
- internal int SessionDeaths => _sessionDeaths;
- internal int TotalDeaths => _totalDeaths;
- internal DateTime StatsStartTime => _statsStartTime;
- internal DateTime LastKillTime => _lastKillTime;
- internal int RareCount { get; set; }
-
- /// Logger for chat output
- /// Callback(totalKills, killsPer5Min, killsPerHour) for UI updates
- /// Callback(elapsed) for UI elapsed time updates
- internal KillTracker(IPluginLogger logger, Action onStatsUpdated, Action onElapsedUpdated)
- {
- _logger = logger;
- _onStatsUpdated = onStatsUpdated;
- _onElapsedUpdated = onElapsedUpdated;
- }
-
- internal void Start()
- {
- _updateTimer = new Timer(Constants.StatsUpdateIntervalMs);
- _updateTimer.Elapsed += UpdateStats;
- _updateTimer.Start();
- }
-
- internal void Stop()
- {
- if (_updateTimer != null)
- {
- _updateTimer.Stop();
- _updateTimer.Dispose();
- _updateTimer = null;
- }
- }
-
- internal bool CheckForKill(string text)
- {
- if (IsKilledByMeMessage(text))
- {
- _totalKills++;
- _lastKillTime = DateTime.Now;
- CalculateKillsPerInterval();
- _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
- return true;
- }
- return false;
- }
-
- internal void OnDeath()
- {
- _sessionDeaths++;
- }
-
- internal void SetTotalDeaths(int totalDeaths)
- {
- _totalDeaths = totalDeaths;
- }
-
- internal void RestartStats()
- {
- _totalKills = 0;
- RareCount = 0;
- _sessionDeaths = 0;
- _statsStartTime = DateTime.Now;
- _killsPer5Min = 0;
- _killsPerHour = 0;
-
- _logger?.Log($"Stats have been reset. Session deaths: {_sessionDeaths}, Total deaths: {_totalDeaths}");
- _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
- }
-
- private void UpdateStats(object sender, ElapsedEventArgs e)
- {
- try
- {
- TimeSpan elapsed = DateTime.Now - _statsStartTime;
- _onElapsedUpdated?.Invoke(elapsed);
-
- CalculateKillsPerInterval();
- _onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
- }
- catch (Exception ex)
- {
- _logger?.Log("Error updating stats: " + ex.Message);
- }
- }
-
- private void CalculateKillsPerInterval()
- {
- double minutesElapsed = (DateTime.Now - _statsStartTime).TotalMinutes;
-
- if (minutesElapsed > 0)
- {
- _killsPer5Min = (_totalKills / minutesElapsed) * 5;
- _killsPerHour = (_totalKills / minutesElapsed) * 60;
- }
- }
-
- private bool IsKilledByMeMessage(string text)
- {
- foreach (string pattern in KillPatterns)
- {
- if (Regex.IsMatch(text, pattern))
- return true;
- }
- return false;
- }
- }
-}
diff --git a/MosswartMassacre/LiveInventoryTracker.cs b/MosswartMassacre/LiveInventoryTracker.cs
deleted file mode 100644
index 4b3546b..0000000
--- a/MosswartMassacre/LiveInventoryTracker.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using Mag.Shared;
-
-namespace MosswartMassacre
-{
- ///
- /// Sends inventory delta events (add/remove/update) via WebSocket
- /// whenever items change in the player's inventory.
- ///
- internal class LiveInventoryTracker
- {
- private readonly IPluginLogger _logger;
- private readonly HashSet _trackedItemIds = new HashSet();
-
- internal LiveInventoryTracker(IPluginLogger logger)
- {
- _logger = logger;
- }
-
- ///
- /// Initialize tracking for all current inventory items.
- /// Called after login or hot reload, after the full inventory dump.
- ///
- internal void Initialize()
- {
- _trackedItemIds.Clear();
- try
- {
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- _trackedItemIds.Add(wo.Id);
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[LiveInv] Error initializing: {ex.Message}");
- }
- }
-
- internal void OnCreateObject(object sender, CreateObjectEventArgs e)
- {
- try
- {
- var item = e.New;
- if (!IsPlayerInventory(item)) return;
- if (_trackedItemIds.Contains(item.Id)) return;
-
- _trackedItemIds.Add(item.Id);
- var mwo = MyWorldObjectCreator.Create(item);
- _ = WebSocket.SendInventoryDeltaAsync("add", mwo);
-
- // Request appraisal if item needs full ID data (spells, combat stats, etc.)
- if (!item.HasIdData && ObjectClassNeedsIdent(item.ObjectClass, item.Name))
- {
- CoreManager.Current.Actions.RequestId(item.Id);
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[LiveInv] Error in OnCreate: {ex.Message}");
- }
- }
-
- internal void OnReleaseObject(object sender, ReleaseObjectEventArgs e)
- {
- try
- {
- var item = e.Released;
- if (!_trackedItemIds.Contains(item.Id)) return;
-
- _trackedItemIds.Remove(item.Id);
- _ = WebSocket.SendInventoryRemoveAsync(item.Id);
- }
- catch (Exception ex)
- {
- _logger?.Log($"[LiveInv] Error in OnRelease: {ex.Message}");
- }
- }
-
- internal void OnChangeObject(object sender, ChangeObjectEventArgs e)
- {
- try
- {
- var item = e.Changed;
- if (!IsPlayerInventory(item))
- {
- // Item left our inventory
- if (_trackedItemIds.Contains(item.Id))
- {
- _trackedItemIds.Remove(item.Id);
- _ = WebSocket.SendInventoryRemoveAsync(item.Id);
- }
- return;
- }
-
- if (!_trackedItemIds.Contains(item.Id))
- {
- // New item appeared via ChangeObject
- _trackedItemIds.Add(item.Id);
- var mwo = MyWorldObjectCreator.Create(item);
- _ = WebSocket.SendInventoryDeltaAsync("add", mwo);
-
- // Request appraisal if item needs full ID data
- if (!item.HasIdData && ObjectClassNeedsIdent(item.ObjectClass, item.Name))
- {
- CoreManager.Current.Actions.RequestId(item.Id);
- }
- }
- else
- {
- // Existing item changed (equip/unequip, stack change, container move)
- var mwo = MyWorldObjectCreator.Create(item);
- _ = WebSocket.SendInventoryDeltaAsync("update", mwo);
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[LiveInv] Error in OnChange: {ex.Message}");
- }
- }
-
- internal void Cleanup()
- {
- _trackedItemIds.Clear();
- }
-
- private static bool ObjectClassNeedsIdent(ObjectClass oc, string name)
- {
- return oc == ObjectClass.Armor
- || oc == ObjectClass.Clothing
- || oc == ObjectClass.MeleeWeapon
- || oc == ObjectClass.MissileWeapon
- || oc == ObjectClass.WandStaffOrb
- || oc == ObjectClass.Jewelry
- || (oc == ObjectClass.Gem && !string.IsNullOrEmpty(name) && name.Contains("Aetheria"))
- || (oc == ObjectClass.Misc && !string.IsNullOrEmpty(name) && name.Contains("Essence"));
- }
-
- private static bool IsPlayerInventory(WorldObject item)
- {
- try
- {
- int containerId = item.Container;
- int charId = CoreManager.Current.CharacterFilter.Id;
-
- // Directly in character's inventory
- if (containerId == charId) return true;
-
- // In a side pack owned by the character
- WorldObject container = CoreManager.Current.WorldFilter[containerId];
- if (container != null &&
- container.ObjectClass == ObjectClass.Container &&
- container.Container == charId)
- {
- return true;
- }
-
- return false;
- }
- catch
- {
- return false;
- }
- }
- }
-}
diff --git a/MosswartMassacre/MainView.cs b/MosswartMassacre/MainView.cs
new file mode 100644
index 0000000..063e7f7
--- /dev/null
+++ b/MosswartMassacre/MainView.cs
@@ -0,0 +1,96 @@
+using System;
+using MyClasses.MetaViewWrappers;
+
+namespace MosswartMassacre
+{
+ internal static class MainView
+ {
+ private static IView View;
+ private static IStaticText lblTotalKills;
+ private static IStaticText lblKillsPer5Min;
+ private static IStaticText lblKillsPerHour;
+ private static IStaticText lblElapsedTime;
+ private static IStaticText lblRareCount;
+ private static IButton btnRestart;
+ private static IButton btnToggleRareMeta;
+
+ public static void ViewInit()
+ {
+ try
+ {
+ // Load the view from the embedded XML resource
+ View = MyClasses.MetaViewWrappers.ViewSystemSelector.CreateViewResource(
+ PluginCore.MyHost, "MosswartMassacre.ViewXML.mainView.xml");
+
+ // Get references to controls
+ lblTotalKills = (IStaticText)View["lblTotalKills"];
+ lblKillsPer5Min = (IStaticText)View["lblKillsPer5Min"];
+ lblKillsPerHour = (IStaticText)View["lblKillsPerHour"];
+ lblElapsedTime = (IStaticText)View["lblElapsedTime"];
+ lblRareCount = (IStaticText)View["lblRareCount"];
+ btnRestart = (IButton)View["btnRestart"];
+ btnRestart.Hit += OnRestartClick;
+ btnToggleRareMeta = (IButton)View["btnToggleRareMeta"];
+ btnToggleRareMeta.Hit += OnToggleRareMetaClick;
+ btnToggleRareMeta.Text = "Meta: ON";
+
+ PluginCore.WriteToChat("View initialized.");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("Error initializing view: " + ex.Message);
+ }
+ }
+
+ public static void ViewDestroy()
+ {
+ try
+ {
+ View.Dispose();
+ PluginCore.WriteToChat("View destroyed.");
+ btnRestart.Hit -= OnRestartClick;
+ btnToggleRareMeta.Hit -= OnToggleRareMetaClick;
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat("Error destroying view: " + ex.Message);
+ }
+ }
+
+ public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
+ {
+ lblTotalKills.Text = $"Total Kills: {totalKills}";
+ lblKillsPer5Min.Text = $"Kills per 5 Min: {killsPer5Min:F2}";
+ lblKillsPerHour.Text = $"Kills per Hour: {killsPerHour:F2}";
+ }
+
+ public static void UpdateElapsedTime(TimeSpan elapsed)
+ {
+ int days = elapsed.Days;
+ int hours = elapsed.Hours;
+ int minutes = elapsed.Minutes;
+ int seconds = elapsed.Seconds;
+
+ if (days > 0)
+ lblElapsedTime.Text = $"Time: {days}d {hours:D2}:{minutes:D2}:{seconds:D2}";
+ else
+ lblElapsedTime.Text = $"Time: {hours:D2}:{minutes:D2}:{seconds:D2}";
+ }
+ public static void UpdateRareCount(int rareCount)
+ {
+ lblRareCount.Text = $"Rare Count: {rareCount}";
+ }
+ private static void OnRestartClick(object sender, EventArgs e)
+ {
+ PluginCore.RestartStats();
+ }
+ private static void OnToggleRareMetaClick(object sender, EventArgs e)
+ {
+ PluginCore.ToggleRareMeta();
+ }
+ public static void SetRareMetaToggleState(bool enabled)
+ {
+ btnToggleRareMeta.Text = enabled ? "Meta: ON" : "Meta: OFF";
+ }
+ }
+}
diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 56d93d1..ac9acd5 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -1,6 +1,5 @@
-
Debug
@@ -14,8 +13,6 @@
8.0
512
true
-
-
true
@@ -32,206 +29,42 @@
pdbonly
true
bin\Release\
- TRACE;VVS_REFERENCED;DECAL_INTEROP
+ TRACE
prompt
4
- x86
- true
-
- lib\0Harmony.dll
- False
-
-
- ..\packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll
-
lib\Decal.Adapter.dll
False
-
- lib\Decal.FileService.dll
-
-
+
False
False
lib\Decal.Interop.Core.DLL
- False
-
-
- False
- False
- lib\Decal.Interop.Filters.DLL
- False
False
False
lib\Decal.Interop.Inject.dll
-
- False
- False
- lib\Decal.Interop.D3DService.DLL
- False
-
-
- False
- False
- lib\Decal.Interop.Input.DLL
- False
-
-
- ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll
- True
- True
-
..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
-
- ..\packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll
- True
- True
-
-
-
- ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll
- True
- True
-
-
- ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll
-
-
- ..\packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll
- True
- True
-
-
- ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll
- True
- True
-
-
- ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll
- True
- True
-
-
- ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll
- True
- True
-
-
-
- ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll
- True
- True
-
-
- ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll
- True
- True
-
-
- ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll
- True
- True
-
-
- ..\packages\System.Linq.4.3.0\lib\net463\System.Linq.dll
- True
- True
-
-
- ..\packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll
- True
- True
-
-
- ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll
- True
- True
-
-
- ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll
- True
- True
-
-
- ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll
- True
- True
-
-
- ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll
- True
- True
-
-
- ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll
- True
- True
-
-
- ..\packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll
- True
- True
-
-
- ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll
- True
- True
-
-
-
- ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll
- True
- True
-
-
- ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll
- True
- True
-
-
- ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll
- True
- True
-
-
- ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll
- True
- True
-
-
- ..\packages\System.Text.RegularExpressions.4.3.0\lib\net463\System.Text.RegularExpressions.dll
- True
- True
-
+
-
- ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll
- True
- True
-
-
+
False
- lib\utank2-i.dll
-
-
- lib\VCS5.dll
+ bin\Debug\utank2-i.dll
lib\VirindiViewService.dll
@@ -241,111 +74,28 @@
-
- Shared\Constants\BoolValueKey.cs
-
-
- Shared\Constants\Dictionaries.cs
-
-
- Shared\Spells\Spell.cs
-
-
- Shared\Constants\DoubleValueKey.cs
-
-
- Shared\Constants\EphemeralAttribute.cs
-
-
- Shared\Constants\IntValueKey.cs
-
-
- Shared\Constants\SendOnLoginAttribute.cs
-
-
- Shared\Constants\ServerOnlyAttribute.cs
-
-
- Shared\Constants\StringValueKey.cs
-
-
- Shared\Debug.cs
-
-
- Shared\DecalProxy.cs
-
-
- Shared\MyWorldObject.cs
-
-
- Shared\MyWorldObjectCreator.cs
-
-
- Shared\PostMessageTools.cs
-
-
- Shared\RateLimiter.cs
-
-
- Shared\SerializableDictionary.cs
-
-
- Shared\Settings\Setting.cs
-
-
- Shared\Settings\SettingsFile.cs
-
-
- Shared\User32.cs
-
-
- Shared\Util.cs
-
-
- Shared\VCS_Connector.cs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
True
True
Resources.resx
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -354,51 +104,11 @@
-
-
-
- Shared\Spells\Spells.csv
-
-
-
- lib\Decal.dll
- False
-
-
- lib\decalnet.dll
- True
-
-
-
-
-
-
-
-
- This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
-
-
-
-
-
-
-
-
- $([System.DateTime]::UtcNow.ToString("yyyy.M.d.HHmm"))
- $(IntermediateOutputPath)CalVer.cs
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MosswartMassacre/MossyInventory.cs b/MosswartMassacre/MossyInventory.cs
deleted file mode 100644
index 299b130..0000000
--- a/MosswartMassacre/MossyInventory.cs
+++ /dev/null
@@ -1,336 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Newtonsoft.Json;
-
-using Mag.Shared;
-
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-using System.Diagnostics;
-using YamlDotNet.Serialization;
-
-namespace MosswartMassacre
-{
- class MossyInventory : IDisposable
- {
-
- private string InventoryFileName
- {
- get
- {
- // 1) Character name
- var characterName = CoreManager.Current.CharacterFilter.Name;
-
- // 2) Plugin folder - handle hot reload scenarios
- string pluginFolder;
- if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
- {
- pluginFolder = PluginCore.AssemblyDirectory;
- }
- else
- {
- pluginFolder = Path.GetDirectoryName(
- System.Reflection.Assembly
- .GetExecutingAssembly()
- .Location
- );
- }
-
- // 3) Character-specific folder path
- var characterFolder = Path.Combine(pluginFolder, characterName);
-
- // 4) Ensure directory exists (can do it here, thread-safe for most single-user plugin cases)
- if (!Directory.Exists(characterFolder))
- Directory.CreateDirectory(characterFolder);
-
- // 5) Return full path to the .json file inside the character folder
- return Path.Combine(characterFolder, $"{characterName}.json");
- }
- }
-
- public MossyInventory()
- {
- try
- {
- CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
- CoreManager.Current.WorldFilter.CreateObject += WorldFilter_CreateObject;
- CoreManager.Current.WorldFilter.ChangeObject += WorldFilter_ChangeObject;
- CoreManager.Current.CharacterFilter.Logoff += CharacterFilter_Logoff;
- PluginCore.WriteToChat($"[INV] {InventoryFileName}");
- PluginCore.WriteToChat("Started MOSSY!");
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[INV] {ex}");
- }
- }
-
- private bool disposed;
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected virtual void Dispose(bool disposing)
- {
- if (disposed) return;
- if (disposing)
- {
- CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
- CoreManager.Current.WorldFilter.CreateObject -= WorldFilter_CreateObject;
- CoreManager.Current.WorldFilter.ChangeObject -= WorldFilter_ChangeObject;
- CoreManager.Current.CharacterFilter.Logoff -= CharacterFilter_Logoff;
- }
- disposed = true;
- }
-
- private bool loginComplete;
- private bool loggedInAndWaitingForIdData;
- private readonly List requestedIds = new List();
-
- private void CharacterFilter_LoginComplete(object sender, EventArgs e)
- {
- try
- {
- loginComplete = true;
-
- // Defensive check - settings might not be initialized yet due to event handler order
- bool inventoryLogEnabled;
- try
- {
- inventoryLogEnabled = PluginSettings.Instance.InventoryLog;
- }
- catch (InvalidOperationException)
- {
- PluginCore.WriteToChat("[INV] Settings not ready, skipping inventory check");
- return;
- }
-
- if (!inventoryLogEnabled)
- return;
-
- if (!File.Exists(InventoryFileName))
- {
- PluginCore.WriteToChat("Requesting id information for all armor/weapon inventory. This will take a few minutes...");
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (!wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
- CoreManager.Current.Actions.RequestId(wo.Id);
- }
- loggedInAndWaitingForIdData = true;
- }
- else
- {
- DumpInventoryToFile(true);
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[INV] {ex}");
- }
- }
-
- private void WorldFilter_CreateObject(object sender, CreateObjectEventArgs e)
- {
- if (!loginComplete) return;
-
- try
- {
- if (!PluginSettings.Instance.InventoryLog) return;
- }
- catch (InvalidOperationException)
- {
- return; // Settings not ready, skip silently
- }
-
- if (!e.New.HasIdData && ObjectClassNeedsIdent(e.New.ObjectClass, e.New.Name)
- && !requestedIds.Contains(e.New.Id)
- && e.New.Container == CoreManager.Current.CharacterFilter.Id)
- {
- requestedIds.Add(e.New.Id);
- CoreManager.Current.Actions.RequestId(e.New.Id);
- }
- }
-
- private void WorldFilter_ChangeObject(object sender, ChangeObjectEventArgs e)
- {
- if (!loginComplete) return;
-
- try
- {
- if (!PluginSettings.Instance.InventoryLog) return;
- }
- catch (InvalidOperationException)
- {
- return; // Settings not ready, skip silently
- }
-
- if (loggedInAndWaitingForIdData)
- {
- bool allHaveId = true;
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (!wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
- {
- allHaveId = false;
- break;
- }
- }
- if (allHaveId)
- {
- loggedInAndWaitingForIdData = false;
- DumpInventoryToFile();
- PluginCore.WriteToChat("Requesting id information for all armor/weapon inventory completed. Log file written.");
- }
- }
- else
- {
- if (!e.Changed.HasIdData && ObjectClassNeedsIdent(e.Changed.ObjectClass, e.Changed.Name)
- && !requestedIds.Contains(e.Changed.Id)
- && e.Changed.Container == CoreManager.Current.CharacterFilter.Id)
- {
- requestedIds.Add(e.Changed.Id);
- CoreManager.Current.Actions.RequestId(e.Changed.Id);
- }
- }
-
- }
-
- private void CharacterFilter_Logoff(object sender, Decal.Adapter.Wrappers.LogoffEventArgs e)
- {
- try
- {
- try
- {
- if (!PluginSettings.Instance.InventoryLog) return;
- }
- catch (InvalidOperationException)
- {
- return; // Settings not ready, skip silently
- }
- DumpInventoryToFile(true); // Request IDs if missing to ensure complete data
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[INV] {ex}");
- }
- }
-
- private void DumpInventoryToFile(bool requestIdsIfMissing = false)
- {
- var previouslySaved = new List();
-
- if (File.Exists(InventoryFileName))
- {
- try
- {
- string oldJson = File.ReadAllText(InventoryFileName);
- previouslySaved = JsonConvert.DeserializeObject>(oldJson)
- ?? new List();
- }
- catch (Exception)
- {
- PluginCore.WriteToChat("Inventory file is corrupt.");
- }
- }
-
- var currentList = new List();
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- // Check to see if we already have some information for this item
- foreach (var prev in previouslySaved)
- {
- if (prev.Id == wo.Id && prev.ObjectClass == (int)wo.ObjectClass)
- {
- // If neither our past nor our current item HadIdData, but it should, lets request it
- if (requestIdsIfMissing && !prev.HasIdData && !wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
- {
- CoreManager.Current.Actions.RequestId(wo.Id);
- currentList.Add(MyWorldObjectCreator.Create(wo));
- }
- else
- {
- // Add the WorldObject to the MyWorldObject data so we have up to date information
- currentList.Add(MyWorldObjectCreator.Combine(prev, wo));
- }
-
- goto end;
- }
- }
-
- if (requestIdsIfMissing && !wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
- CoreManager.Current.Actions.RequestId(wo.Id);
-
- currentList.Add(MyWorldObjectCreator.Create(wo));
-
- end: ;
- }
-
- var fi = new FileInfo(InventoryFileName);
- if (fi.Directory != null && !fi.Directory.Exists)
- fi.Directory.Create();
-
- string json = JsonConvert.SerializeObject(currentList, Formatting.Indented);
- File.WriteAllText(InventoryFileName, json);
-
- // Send full inventory via WebSocket
- if (PluginCore.WebSocketEnabled)
- {
- _ = WebSocket.SendFullInventoryAsync(currentList);
- PluginCore.WriteToChat("Inventory sent to MosswartOverlord");
- }
- }
-
- private bool ObjectClassNeedsIdent(ObjectClass oc, string name)
- {
- return oc == ObjectClass.Armor
- || oc == ObjectClass.Clothing
- || oc == ObjectClass.MeleeWeapon
- || oc == ObjectClass.MissileWeapon
- || oc == ObjectClass.WandStaffOrb
- || oc == ObjectClass.Jewelry
- || (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/NavRoute.cs b/MosswartMassacre/NavRoute.cs
deleted file mode 100644
index 6f3ccfa..0000000
--- a/MosswartMassacre/NavRoute.cs
+++ /dev/null
@@ -1,412 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.IO;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-
-namespace MosswartMassacre
-{
- public class NavWaypoint
- {
- public double NS { get; set; }
- public double EW { get; set; }
- public double Z { get; set; }
- public int Type { get; set; }
- public NavWaypoint Previous { get; set; }
- }
-
- public class NavRoute : IDisposable
- {
- private bool disposed = false;
- private List waypoints = new List();
- private List lineObjects = new List();
- private Color routeColor;
- private bool isVisible = false;
-
- public string FilePath { get; private set; }
- public string FileName => Path.GetFileNameWithoutExtension(FilePath);
- public bool IsVisible => isVisible;
- public int WaypointCount => waypoints.Count;
-
- public NavRoute(string filePath, Color color)
- {
- FilePath = filePath;
- routeColor = color;
- }
-
- public bool LoadFromFile()
- {
- try
- {
- ClearRoute();
- waypoints.Clear();
-
- if (!File.Exists(FilePath))
- {
- PluginCore.WriteToChat($"Nav file not found: {FilePath}");
- return false;
- }
-
- PluginCore.WriteToChat($"Navigation: Loading {FileName}...");
-
- using (StreamReader sr = File.OpenText(FilePath))
- {
- // Read header
- string header = sr.ReadLine();
- if (string.IsNullOrEmpty(header) || !header.StartsWith("uTank2 NAV"))
- {
- PluginCore.WriteToChat($"Navigation: Invalid file format - {FileName}");
- return false;
- }
-
- // Read nav type
- string navTypeLine = sr.ReadLine();
- if (string.IsNullOrEmpty(navTypeLine) || !int.TryParse(navTypeLine.Trim(), out int navType))
- {
- PluginCore.WriteToChat($"Navigation: Failed to parse route type - {FileName}");
- return false;
- }
-
- string navTypeDescription = "";
- switch (navType)
- {
- case 0:
- navTypeDescription = "Linear";
- break;
- case 1:
- navTypeDescription = "Circular";
- break;
- case 2:
- navTypeDescription = "Linear";
- break;
- case 3:
- navTypeDescription = "Target (follow player/object)";
- break;
- case 4:
- navTypeDescription = "Once";
- break;
- default:
- navTypeDescription = $"Unknown ({navType})";
- PluginCore.WriteToChat($"Navigation: Unknown route type {navType} in {FileName}");
- break;
- }
-
- // Handle target nav (type 3) - follows a specific player/object
- if (navType == 3)
- {
- if (sr.EndOfStream)
- {
- PluginCore.WriteToChat($"Navigation: Target route file is empty - {FileName}");
- return false;
- }
-
- string targetName = sr.ReadLine();
- if (sr.EndOfStream)
- {
- PluginCore.WriteToChat($"Navigation: Target route missing target ID - {FileName}");
- return false;
- }
-
- string targetIdLine = sr.ReadLine();
-
- PluginCore.WriteToChat($"Navigation: Target route '{targetName}' cannot be visualized");
- return true; // Successfully loaded but can't visualize
- }
-
- // Read record count
- string recordCountLine = sr.ReadLine();
- if (string.IsNullOrEmpty(recordCountLine) || !int.TryParse(recordCountLine.Trim(), out int recordCount))
- {
- PluginCore.WriteToChat($"Navigation: Failed to parse waypoint count - {FileName}");
- return false;
- }
-
- if (recordCount <= 0 || recordCount > 10000) // Sanity check
- {
- PluginCore.WriteToChat($"Navigation: Invalid waypoint count {recordCount} - {FileName}");
- return false;
- }
-
- NavWaypoint previous = null;
- int waypointsRead = 0;
-
- while (!sr.EndOfStream && waypointsRead < recordCount)
- {
- // Read waypoint type
- string waypointTypeLine = sr.ReadLine();
-
- if (string.IsNullOrEmpty(waypointTypeLine) || !int.TryParse(waypointTypeLine.Trim(), out int waypointType))
- {
- PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} in {FileName}");
- break; // Skip this waypoint, don't fail entirely
- }
-
- // Read coordinates (all waypoint types have EW, NS, Z, Unknown)
- string ewLine = sr.ReadLine();
- string nsLine = sr.ReadLine();
- string zLine = sr.ReadLine();
- string unknownLine = sr.ReadLine(); // Unknown value (always 0)
-
- if (string.IsNullOrEmpty(ewLine) || string.IsNullOrEmpty(nsLine) || string.IsNullOrEmpty(zLine) || string.IsNullOrEmpty(unknownLine))
- {
- PluginCore.WriteToChat($"Navigation: Missing coordinates at waypoint {waypointsRead + 1} in {FileName}");
- break;
- }
-
- if (!double.TryParse(ewLine.Trim(), out double ew) ||
- !double.TryParse(nsLine.Trim(), out double ns) ||
- !double.TryParse(zLine.Trim(), out double z))
- {
- PluginCore.WriteToChat($"Navigation: Invalid coordinates at waypoint {waypointsRead + 1} in {FileName}");
- break; // Skip this waypoint
- }
-
- var waypoint = new NavWaypoint
- {
- NS = ns,
- EW = ew,
- Z = z,
- Type = waypointType,
- Previous = previous
- };
-
- waypoints.Add(waypoint);
- previous = waypoint;
- waypointsRead++;
-
- // Skip additional data based on waypoint type
- if (!SkipWaypointData(sr, waypointType))
- {
- PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} data in {FileName}");
- break; // Don't continue if we can't parse properly
- }
- }
-
- if (waypoints.Count > 0)
- {
- PluginCore.WriteToChat($"Navigation: Loaded {FileName} ({waypoints.Count} waypoints)");
- }
- else
- {
- PluginCore.WriteToChat($"Navigation: No valid waypoints found in {FileName}");
- }
-
- return waypoints.Count > 0;
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Navigation: Error loading {FileName} - {ex.Message}");
- return false;
- }
- }
-
- private bool SkipWaypointData(StreamReader sr, int waypointType)
- {
- try
- {
- // Skip additional lines based on waypoint type (base 4 lines already read)
- switch (waypointType)
- {
- case 0: // Point - no additional data (4 lines total)
- break;
- case 1: // Portal - 5 additional lines (9 lines total)
- sr.ReadLine(); // Name
- sr.ReadLine(); // ObjectClass
- sr.ReadLine(); // "true"
- sr.ReadLine(); // PortalNS
- sr.ReadLine(); // PortalEW
- sr.ReadLine(); // PortalZ
- break;
- case 2: // Recall - 1 additional line (5 lines total)
- sr.ReadLine(); // RecallSpellId
- break;
- case 3: // Pause - 1 additional line (5 lines total)
- sr.ReadLine(); // Pause milliseconds
- break;
- case 4: // ChatCommand - 1 additional line (5 lines total)
- sr.ReadLine(); // Message
- break;
- case 5: // OpenVendor - 2 additional lines (6 lines total)
- sr.ReadLine(); // Id
- sr.ReadLine(); // Name
- break;
- case 6: // Portal2 - same as Portal (9 lines total)
- sr.ReadLine(); // Name
- sr.ReadLine(); // ObjectClass
- sr.ReadLine(); // "true"
- sr.ReadLine(); // PortalNS
- sr.ReadLine(); // PortalEW
- sr.ReadLine(); // PortalZ
- break;
- case 7: // UseNPC - 5 additional lines (9 lines total)
- sr.ReadLine(); // Name
- sr.ReadLine(); // ObjectClass
- sr.ReadLine(); // "true"
- sr.ReadLine(); // NpcEW
- sr.ReadLine(); // NpcNS
- sr.ReadLine(); // NpcZ
- break;
- case 8: // Checkpoint - no additional data (4 lines total)
- break;
- case 9: // Jump - 3 additional lines (7 lines total)
- sr.ReadLine(); // Heading
- sr.ReadLine(); // ShiftJump
- sr.ReadLine(); // Milliseconds
- break;
- default:
- // Unknown waypoint type - skip silently
- break;
- }
- return true;
- }
- catch
- {
- // Silently handle parsing errors
- return false;
- }
- }
-
- public void Show()
- {
- if (isVisible) return;
-
- if (waypoints.Count == 0)
- {
- PluginCore.WriteToChat($"Navigation: No waypoints to visualize in {FileName}");
- return;
- }
-
- CreateLineObjects();
- isVisible = true;
- PluginCore.WriteToChat($"Navigation: Showing {FileName} ({waypoints.Count} waypoints)");
- }
-
- public void Hide()
- {
- if (!isVisible) return;
-
- ClearRoute();
- isVisible = false;
- PluginCore.WriteToChat($"Navigation: Hidden {FileName}");
- }
-
- private void CreateLineObjects()
- {
- try
- {
- // Check D3DService availability
- if (CoreManager.Current?.D3DService == null)
- {
- PluginCore.WriteToChat($"Navigation: 3D service unavailable");
- return;
- }
-
- // Limit the number of lines to prevent lag
- int maxLines = Math.Min(waypoints.Count - 1, 500); // Max 500 lines
-
- int linesCreated = 0;
- for (int i = 1; i <= maxLines; i++)
- {
- var current = waypoints[i];
- var previous = waypoints[i - 1];
-
- if (CreateLineBetweenWaypoints(previous, current))
- {
- linesCreated++;
- }
-
- // Add small delay every 50 lines to prevent UI freezing
- if (i % 50 == 0)
- {
- System.Threading.Thread.Sleep(1);
- }
- }
-
- if (waypoints.Count > 501)
- {
- PluginCore.WriteToChat($"Navigation: Large route - showing {maxLines} of {waypoints.Count} segments");
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Navigation: Error creating visualization - {ex.Message}");
- }
- }
-
- private bool CreateLineBetweenWaypoints(NavWaypoint from, NavWaypoint to)
- {
- try
- {
- // Calculate distance
- double distance = Math.Sqrt(
- Math.Pow((to.NS - from.NS) * 240, 2) +
- Math.Pow((to.EW - from.EW) * 240, 2) +
- Math.Pow((to.Z - from.Z) * 240, 2)
- );
-
- if (distance <= 0) return false;
-
- // Create D3D line object
- var lineObj = CoreManager.Current.D3DService.NewD3DObj();
- if (lineObj == null) return false;
-
- lineObj.SetShape(D3DShape.Cube);
- lineObj.Color = routeColor.ToArgb();
-
- // Position at midpoint between waypoints
- float midNS = (float)(from.NS + to.NS) / 2;
- float midEW = (float)(from.EW + to.EW) / 2;
- float midZ = (float)((from.Z + to.Z) * 120) + 0.1f; // Slightly higher than UtilityBelt
-
- lineObj.Anchor(midNS, midEW, midZ);
-
- // Orient toward destination
- float orientNS = (float)from.NS;
- float orientEW = (float)from.EW;
- float orientZ = (float)(from.Z * 240) + 0.1f;
-
- lineObj.OrientToCoords(orientNS, orientEW, orientZ, true);
-
- // Scale to create line effect
- lineObj.ScaleX = 0.3f; // Slightly thicker than UtilityBelt
- lineObj.ScaleZ = 0.3f;
- lineObj.ScaleY = (float)distance;
-
- lineObj.Visible = true;
- lineObjects.Add(lineObj);
-
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private void ClearRoute()
- {
- foreach (var obj in lineObjects)
- {
- try
- {
- obj.Visible = false;
- obj.Dispose();
- }
- catch { }
- }
- lineObjects.Clear();
- }
-
- public void Dispose()
- {
- if (!disposed)
- {
- ClearRoute();
- waypoints.Clear();
- disposed = true;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/NavVisualization.cs b/MosswartMassacre/NavVisualization.cs
deleted file mode 100644
index 824462f..0000000
--- a/MosswartMassacre/NavVisualization.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.IO;
-using System.Linq;
-using Decal.Adapter;
-using Microsoft.Win32;
-
-namespace MosswartMassacre
-{
- public class NavVisualization : IDisposable
- {
- private bool disposed = false;
- private NavRoute currentRoute = null;
- private string vtankProfilesDirectory = "";
- private List availableNavFiles = new List();
-
- // Default comparison route color (red)
- private readonly Color comparisonRouteColor = Color.FromArgb(255, 255, 100, 100);
-
- public bool IsEnabled { get; private set; } = false;
- public bool HasRouteLoaded => currentRoute != null && currentRoute.WaypointCount > 0;
- public string CurrentRouteFile => currentRoute?.FileName ?? "None";
- public List AvailableNavFiles => availableNavFiles.ToList();
-
- public event EventHandler RouteChanged;
-
- public NavVisualization()
- {
- InitializeVTankDirectory();
- RefreshNavFileList();
- }
-
- private void InitializeVTankDirectory()
- {
- try
- {
- // First, check if user has configured a custom path
- if (!string.IsNullOrEmpty(PluginSettings.Instance?.VTankProfilesPath))
- {
- vtankProfilesDirectory = PluginSettings.Instance.VTankProfilesPath;
- return;
- }
-
- // Try to get VTank directory from Windows Registry (same method as UtilityBelt)
- 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.IsNullOrEmpty(profilePath))
- {
- vtankProfilesDirectory = profilePath;
- return;
- }
- }
- }
- catch
- {
- }
-
- // Fall back to default path
- vtankProfilesDirectory = defaultPath;
- // Using default path - user can configure in Settings if needed
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[NavViz] Error finding VTank directory: {ex.Message}");
- vtankProfilesDirectory = "";
- }
- }
-
- ///
- /// Scan VTank directory for .nav files and populate available routes list
- /// Filters out follow files and temporary files, sorts alphabetically
- ///
- public void RefreshNavFileList()
- {
- // Re-initialize directory in case settings changed
- InitializeVTankDirectory();
-
- availableNavFiles.Clear();
-
-
- if (string.IsNullOrEmpty(vtankProfilesDirectory))
- {
- PluginCore.WriteToChat("VTank directory not configured. Set path in Settings tab.");
- return;
- }
-
- if (!Directory.Exists(vtankProfilesDirectory))
- {
- PluginCore.WriteToChat($"VTank directory not found: {vtankProfilesDirectory}");
- return;
- }
-
- try
- {
- // Get all files and filter for .nav files only, excluding follow/temporary files
- var allFiles = Directory.GetFiles(vtankProfilesDirectory);
- var navFiles = allFiles
- .Where(file => Path.GetExtension(file).Equals(".nav", StringComparison.OrdinalIgnoreCase))
- .Select(file => Path.GetFileNameWithoutExtension(file))
- .Where(name => !string.IsNullOrEmpty(name) &&
- !name.StartsWith("follow", StringComparison.OrdinalIgnoreCase) &&
- !name.StartsWith("--", StringComparison.OrdinalIgnoreCase))
- .OrderBy(name => name)
- .ToList();
-
- availableNavFiles.AddRange(navFiles);
-
- // Only report summary - no need to spam chat with every file
- if (navFiles.Count > 0)
- {
- PluginCore.WriteToChat($"Navigation: Found {navFiles.Count} route files");
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Navigation: Error scanning files - {ex.Message}");
- }
- }
-
- ///
- /// Load a specific navigation route file for visualization
- /// Clears current route if "None" specified, otherwise loads .nav file
- ///
- /// Name of .nav file (without extension) or "None"
- /// True if route loaded successfully, false otherwise
- public bool LoadRoute(string navFileName)
- {
- try
- {
- // Clear current route
- if (currentRoute != null)
- {
- currentRoute.Dispose();
- currentRoute = null;
- }
-
- if (string.IsNullOrEmpty(navFileName) || navFileName == "None")
- {
- RouteChanged?.Invoke(this, EventArgs.Empty);
- return true;
- }
-
- string fullPath = Path.Combine(vtankProfilesDirectory, navFileName + ".nav");
-
- if (!File.Exists(fullPath))
- {
- PluginCore.WriteToChat($"Navigation file '{navFileName}' not found");
- return false;
- }
-
- currentRoute = new NavRoute(fullPath, comparisonRouteColor);
-
- if (!currentRoute.LoadFromFile())
- {
- currentRoute.Dispose();
- currentRoute = null;
- return false;
- }
-
- // Show route if visualization is enabled
- if (IsEnabled)
- {
- currentRoute.Show();
- }
-
- RouteChanged?.Invoke(this, EventArgs.Empty);
- return true;
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Navigation: Failed to load '{navFileName}' - {ex.Message}");
- return false;
- }
- }
-
- ///
- /// Enable or disable navigation route visualization in 3D world
- /// Shows/hides the currently loaded route based on enabled state
- ///
- /// True to show route lines, false to hide
- public void SetEnabled(bool enabled)
- {
- // No change needed if already in desired state
- if (IsEnabled == enabled) return;
-
- IsEnabled = enabled;
-
- if (currentRoute != null)
- {
- if (enabled)
- {
- currentRoute.Show();
- }
- else
- {
- currentRoute.Hide();
- }
- }
- else
- {
- }
-
- PluginCore.WriteToChat($"Navigation visualization {(enabled ? "enabled" : "disabled")}");
- }
-
- public void ToggleEnabled()
- {
- SetEnabled(!IsEnabled);
- }
-
- public string GetStatus()
- {
- if (currentRoute == null)
- return "No route loaded";
-
- string status = $"{currentRoute.FileName} ({currentRoute.WaypointCount} points)";
- if (IsEnabled && currentRoute.IsVisible)
- status += " - Visible";
- else if (IsEnabled)
- status += " - Hidden";
- else
- status += " - Disabled";
-
- return status;
- }
-
- public void Dispose()
- {
- if (!disposed)
- {
- if (currentRoute != null)
- {
- currentRoute.Dispose();
- currentRoute = null;
- }
- disposed = true;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index b78ebb6..95e1858 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -1,332 +1,59 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
-using System.Threading.Tasks;
+using System.Text.RegularExpressions;
using System.Timers;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
-using MosswartMassacre.Views;
-using Mag.Shared.Constants;
namespace MosswartMassacre
{
[FriendlyName("Mosswart Massacre")]
- public class PluginCore : PluginBase, IPluginLogger, IGameStats
+ public class PluginCore : PluginBase
{
- // Hot Reload Support Properties
- private static string _assemblyDirectory = null;
- public static string AssemblyDirectory
- {
- get
- {
- if (_assemblyDirectory == null)
- {
- try
- {
- _assemblyDirectory = System.IO.Path.GetDirectoryName(typeof(PluginCore).Assembly.Location);
- }
- catch
- {
- _assemblyDirectory = Environment.CurrentDirectory;
- }
- }
- return _assemblyDirectory;
- }
- set
- {
- _assemblyDirectory = value;
- }
- }
- public static bool IsHotReload { get; set; }
-
internal static PluginHost MyHost;
- // Static bridge properties for VVSTabbedMainView (reads from manager instances)
- private static InventoryMonitor _staticInventoryMonitor;
- internal static int cachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
- private static KillTracker _staticKillTracker;
- internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
- internal static double killsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
- internal static int sessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
- internal static int totalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
- internal static DateTime statsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
- internal static DateTime lastKillTime => _staticKillTracker?.LastKillTime ?? DateTime.Now;
-
- // IGameStats explicit implementation (for WebSocket telemetry)
- int IGameStats.TotalKills => _staticKillTracker?.TotalKills ?? 0;
- double IGameStats.KillsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
- int IGameStats.SessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
- int IGameStats.TotalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
- int IGameStats.CachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
- string IGameStats.CharTag => CharTag;
- DateTime IGameStats.StatsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
-
- private static Timer vitalsTimer;
- private static System.Windows.Forms.Timer commandTimer;
- private static Timer characterStatsTimer;
- private static Timer _updateCheckTimer;
- private static readonly Queue pendingCommands = new Queue();
- private static RareTracker _staticRareTracker;
- public static bool RareMetaEnabled
- {
- get => _staticRareTracker?.RareMetaEnabled ?? true;
- set { if (_staticRareTracker != null) _staticRareTracker.RareMetaEnabled = value; }
- }
-
- // VVS View Management
- private static class ViewManager
- {
- public static void ViewInit()
- {
- Views.VVSTabbedMainView.ViewInit();
- }
-
- public static void ViewDestroy()
- {
- Views.VVSTabbedMainView.ViewDestroy();
- }
-
- public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
- {
- Views.VVSTabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
- }
-
- public static void UpdateElapsedTime(TimeSpan elapsed)
- {
- Views.VVSTabbedMainView.UpdateElapsedTime(elapsed);
- }
-
- public static void UpdateRareCount(int rareCount)
- {
- Views.VVSTabbedMainView.UpdateRareCount(rareCount);
- }
-
- public static void SetRareMetaToggleState(bool enabled)
- {
- Views.VVSTabbedMainView.SetRareMetaToggleState(enabled);
- }
-
- public static void RefreshSettingsFromConfig()
- {
- Views.VVSTabbedMainView.RefreshSettingsFromConfig();
- }
-
- public static void RestoreWindowPosition()
- {
- Views.VVSTabbedMainView.RestorePosition();
- }
-
- public static void RefreshUpdateStatus()
- {
- Views.VVSTabbedMainView.RefreshUpdateStatus();
- }
- }
+ internal static int totalKills = 0;
+ internal static int rareCount = 0;
+ internal static DateTime lastKillTime = DateTime.Now;
+ internal static double killsPer5Min = 0;
+ internal static double killsPerHour = 0;
+ internal static DateTime statsStartTime = DateTime.Now;
+ internal static Timer updateTimer;
+ public static bool RareMetaEnabled { get; set; } = true;
+ public static bool RemoteCommandsEnabled { get; set; } = false;
+ public static bool HttpServerEnabled { get; set; } = false;
public static string CharTag { get; set; } = "";
- public static bool WebSocketEnabled { get; set; } = false;
- public bool InventoryLogEnabled { get; set; } = false;
- public static bool AggressiveChatStreamingEnabled { get; set; } = true;
- private MossyInventory _inventoryLogger;
- public static NavVisualization navVisualization;
- public static ChestLooter chestLooter;
-
- // Quest Management for always-on quest streaming
- public static QuestManager questManager;
-
+ public static bool TelemetryEnabled { get; set; } = false;
+ private static Queue rareMessageQueue = new Queue();
+ private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue _chatQueue = new Queue();
- // Managers
- private KillTracker _killTracker;
- private RareTracker _rareTracker;
- private InventoryMonitor _inventoryMonitor;
- private ChatEventRouter _chatEventRouter;
- private GameEventRouter _gameEventRouter;
- private QuestStreamingService _questStreamingService;
- private CommandRouter _commandRouter;
- private LiveInventoryTracker _liveInventoryTracker;
-
protected override void Startup()
{
try
{
- // Set MyHost - for hot reload scenarios, Host might be null
- if (Host != null)
- {
- MyHost = Host;
- }
- else if (MyHost == null)
- {
- // Hot reload fallback - this is okay, WriteToChat will handle it
- MyHost = null;
- }
-
- // Check if this is a hot reload (flag for post-init handling)
- var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3;
- var needsHotReload = IsHotReload || isCharacterLoaded;
+ MyHost = Host;
- // Clean up old event subscriptions to prevent duplicates on hot reload.
- // C# -= with a non-subscribed handler is a no-op, so safe on first load.
- if (_chatEventRouter != null)
- CoreManager.Current.ChatBoxMessage -= new EventHandler(_chatEventRouter.OnChatText);
- CoreManager.Current.ChatBoxMessage -= new EventHandler(ChatEventRouter.AllChatText);
- CoreManager.Current.CommandLineText -= OnChatCommand;
- CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
- CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
- CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
- CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
- CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
- if (_inventoryMonitor != null)
- {
- CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate;
- CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
- CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
- }
- if (_liveInventoryTracker != null)
- {
- CoreManager.Current.WorldFilter.CreateObject -= _liveInventoryTracker.OnCreateObject;
- CoreManager.Current.WorldFilter.ReleaseObject -= _liveInventoryTracker.OnReleaseObject;
- CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
- }
- if (_gameEventRouter != null)
- CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
- WebSocket.OnServerCommand -= HandleServerCommand;
+ WriteToChat("Mosswart Massacre has started!");
- // Stop old timers before recreating (prevents timer leaks on hot reload)
- _killTracker?.Stop();
- if (vitalsTimer != null)
- {
- vitalsTimer.Stop();
- vitalsTimer.Dispose();
- vitalsTimer = null;
- }
- if (commandTimer != null)
- {
- commandTimer.Stop();
- commandTimer.Dispose();
- commandTimer = null;
- }
- if (_updateCheckTimer != null)
- {
- _updateCheckTimer.Stop();
- _updateCheckTimer.Dispose();
- _updateCheckTimer = null;
- }
-
- // Initialize kill tracker (owns the 1-sec stats timer)
- _killTracker = new KillTracker(
- this,
- (kills, per5, perHr) => ViewManager.UpdateKillStats(kills, per5, perHr),
- elapsed => ViewManager.UpdateElapsedTime(elapsed));
- _staticKillTracker = _killTracker;
- _killTracker.Start();
-
- // Initialize inventory monitor (taper tracking)
- _inventoryMonitor = new InventoryMonitor(this);
- _staticInventoryMonitor = _inventoryMonitor;
-
- // Initialize live inventory tracker (delta WebSocket messages)
- _liveInventoryTracker = new LiveInventoryTracker(this);
-
- // Initialize chat event router (rareTracker set later in LoginComplete)
- _chatEventRouter = new ChatEventRouter(
- this, _killTracker, null,
- count => ViewManager.UpdateRareCount(count),
- msg => MyHost?.Actions.InvokeChatParser($"/a {msg}"));
-
- // Initialize game event router
- _gameEventRouter = new GameEventRouter(this);
-
- // Note: Startup messages will appear after character login
- // Subscribe to events
- CoreManager.Current.ChatBoxMessage += new EventHandler(_chatEventRouter.OnChatText);
- CoreManager.Current.ChatBoxMessage += new EventHandler(ChatEventRouter.AllChatText);
+ // Subscribe to chat message event
+ CoreManager.Current.ChatBoxMessage += new EventHandler(OnChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
- CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
- CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
- CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
- CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
- CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
- CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
- CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
- CoreManager.Current.WorldFilter.CreateObject += _liveInventoryTracker.OnCreateObject;
- CoreManager.Current.WorldFilter.ReleaseObject += _liveInventoryTracker.OnReleaseObject;
- CoreManager.Current.WorldFilter.ChangeObject += _liveInventoryTracker.OnChangeObject;
- // Initialize VVS view after character login
- ViewManager.ViewInit();
+ // Initialize the timer
+ updateTimer = new Timer(1000); // Update every second
+ updateTimer.Elapsed += UpdateStats;
+ updateTimer.Start();
- // Initialize vitals streaming timer
- vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs);
- vitalsTimer.Elapsed += SendVitalsUpdate;
- vitalsTimer.Start();
-
- // Initialize command processing timer (Windows Forms timer for main thread)
- commandTimer = new System.Windows.Forms.Timer();
- commandTimer.Interval = Constants.CommandProcessIntervalMs;
- commandTimer.Tick += ProcessPendingCommands;
- commandTimer.Start();
-
- // Note: View initialization moved to LoginComplete for VVS compatibility
-
- // Initialize character stats and hook ServerDispatch early
- // 0x0013 (character properties with luminance) fires DURING login,
- // BEFORE LoginComplete — must hook here to catch it
- CharacterStats.Init(this);
- CoreManager.Current.EchoFilter.ServerDispatch += _gameEventRouter.OnServerDispatch;
+ // Initialize the view (UI)
+ MainView.ViewInit();
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface
vTank.Enable();
- // Set logger for WebSocket
- WebSocket.SetLogger(this);
- WebSocket.SetGameStats(this);
- //lyssna på commands
- WebSocket.OnServerCommand += HandleServerCommand;
- //starta inventory. Hanterar subscriptions i den med
-
- _inventoryLogger = new MossyInventory();
-
- // Initialize navigation visualization system
- navVisualization = new NavVisualization();
-
- // Initialize command router
- _commandRouter = new CommandRouter();
- RegisterCommands();
-
- // Note: ChestLooter is initialized in LoginComplete after PluginSettings.Initialize()
-
- // Note: DECAL Harmony patches will be initialized in LoginComplete event
- // where the chat system is available for error messages
-
- // Hot reload: run after all core objects are initialized
- if (needsHotReload && isCharacterLoaded)
- {
- try
- {
- WriteToChat("[INFO] Hot reload detected - reinitializing plugin");
- InitializeForHotReload();
- WriteToChat("[INFO] Hot reload initialization complete");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Hot reload initialization failed: {ex.Message}");
- }
- }
-
- // 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();
-
}
catch (Exception ex)
{
@@ -339,104 +66,26 @@ namespace MosswartMassacre
try
{
PluginSettings.Save();
- WriteToChat("Mosswart Massacre is shutting down. Bye!");
-
-
+ if (TelemetryEnabled)
+ Telemetry.Stop(); // ensure no dangling timer / HttpClient
+ WriteToChat("Mosswart Massacre is shutting down...");
// Unsubscribe from chat message event
- CoreManager.Current.ChatBoxMessage -= new EventHandler(_chatEventRouter.OnChatText);
+ CoreManager.Current.ChatBoxMessage -= new EventHandler(OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
- CoreManager.Current.ChatBoxMessage -= new EventHandler(ChatEventRouter.AllChatText);
- CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
- CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
- CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
- CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
- CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
- // Unsubscribe inventory monitor
- if (_inventoryMonitor != null)
+
+ // Stop and dispose of the timer
+ if (updateTimer != null)
{
- CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate;
- CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
- CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
- }
- // Unsubscribe from server dispatch
- CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
-
- // Stop kill tracker
- _killTracker?.Stop();
-
- if (vitalsTimer != null)
- {
- vitalsTimer.Stop();
- vitalsTimer.Dispose();
- vitalsTimer = null;
- }
-
- if (commandTimer != null)
- {
- commandTimer.Stop();
- commandTimer.Dispose();
- commandTimer = null;
- }
-
- // Stop quest streaming service
- _questStreamingService?.Stop();
- _questStreamingService = null;
-
- // Stop and dispose character stats timer
- if (characterStatsTimer != null)
- {
- characterStatsTimer.Stop();
- characterStatsTimer.Elapsed -= OnCharacterStatsUpdate;
- characterStatsTimer.Dispose();
- characterStatsTimer = null;
- }
-
- if (_updateCheckTimer != null)
- {
- _updateCheckTimer.Stop();
- _updateCheckTimer.Dispose();
- _updateCheckTimer = null;
- }
-
- // Dispose quest manager
- if (questManager != null)
- {
- questManager.Dispose();
- questManager = null;
+ updateTimer.Stop();
+ updateTimer.Dispose();
+ updateTimer = null;
}
// Clean up the view
- ViewManager.ViewDestroy();
+ MainView.ViewDestroy();
//Disable vtank interface
vTank.Disable();
- // sluta lyssna på commands
- WebSocket.OnServerCommand -= HandleServerCommand;
- WebSocket.Stop();
- //shutdown inv
- _inventoryLogger.Dispose();
-
- // Clean up navigation visualization
- if (navVisualization != null)
- {
- navVisualization.Dispose();
- navVisualization = null;
- }
-
- // Clean up taper tracking
- _inventoryMonitor?.Cleanup();
-
- // Clean up live inventory tracker
- if (_liveInventoryTracker != null)
- {
- CoreManager.Current.WorldFilter.CreateObject -= _liveInventoryTracker.OnCreateObject;
- CoreManager.Current.WorldFilter.ReleaseObject -= _liveInventoryTracker.OnReleaseObject;
- CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
- _liveInventoryTracker.Cleanup();
- }
-
- // Clean up Harmony patches
- DecalHarmonyClean.Cleanup();
MyHost = null;
}
@@ -449,433 +98,117 @@ namespace MosswartMassacre
{
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
- WriteToChat("Mosswart Massacre has started!");
-
-
-
PluginSettings.Initialize(); // Safe to call now
- // Initialize chest looter system (needs PluginSettings to be ready)
- try
- {
- chestLooter = new ChestLooter(CoreManager.Current, PluginSettings.Instance.ChestLooterSettings);
- chestLooter.Initialize();
- chestLooter.StatusChanged += (sender, status) =>
- {
- VVSTabbedMainView.UpdateChestLooterStatus(status);
- };
- }
- catch (Exception ex)
- {
- WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
- }
-
- // Initialize rare tracker and wire to chat router
- _rareTracker = new RareTracker(this);
- _staticRareTracker = _rareTracker;
- _chatEventRouter.SetRareTracker(_rareTracker);
-
// Apply the values
- _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
- WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
+ RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
+ HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
+ TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
CharTag = PluginSettings.Instance.CharTag;
- ViewManager.SetRareMetaToggleState(RareMetaEnabled);
- ViewManager.RefreshSettingsFromConfig(); // Refresh all UI settings after loading
- if (WebSocketEnabled)
- WebSocket.Start();
+ MainView.SetRareMetaToggleState(RareMetaEnabled);
+ if (TelemetryEnabled)
+ Telemetry.Start();
- // Initialize Harmony patches using UtilityBelt's loaded DLL
- try
- {
- bool success = DecalHarmonyClean.Initialize();
- if (success)
- WriteToChat("[OK] Plugin message interception active");
- else
- WriteToChat("[FAIL] Could not initialize message interception");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
- }
- // Initialize death tracking
- _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
-
- // Initialize cached Prismatic Taper count
- _inventoryMonitor.Initialize();
-
- // Initialize live inventory tracking (after full inventory dump)
- _liveInventoryTracker?.Initialize();
-
- // Initialize quest manager for always-on quest streaming
- try
- {
- questManager = new QuestManager();
-
- // Trigger full quest data refresh (same as clicking refresh button)
- Views.FlagTrackerView.RefreshQuestData();
-
- // Initialize quest streaming service (30 seconds)
- _questStreamingService = new QuestStreamingService(this);
- _questStreamingService.Start();
-
- WriteToChat("[OK] Quest streaming initialized with full data refresh");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}");
- }
-
- // Start character stats streaming
- // Note: Init() and ServerDispatch hook are in Startup() so we catch
- // 0x0013 (luminance/properties) which fires BEFORE LoginComplete
- try
- {
- // Start 10-minute character stats timer
- characterStatsTimer = new Timer(Constants.CharacterStatsIntervalMs);
- characterStatsTimer.Elapsed += OnCharacterStatsUpdate;
- characterStatsTimer.AutoReset = true;
- characterStatsTimer.Start();
-
- // Send initial stats after 5-second delay (let CharacterFilter populate)
- var initialDelay = new Timer(Constants.LoginDelayMs);
- initialDelay.AutoReset = false;
- initialDelay.Elapsed += (s, args) =>
- {
- CharacterStats.CollectAndSend();
- ((Timer)s).Dispose();
- };
- initialDelay.Start();
-
- WriteToChat("[OK] Character stats streaming initialized (10-min interval)");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Character stats initialization failed: {ex.Message}");
- }
}
-
- private void InitializeForHotReload()
+ private void OnChatText(object sender, ChatTextInterceptEventArgs e)
{
- // This method handles initialization that depends on character being logged in
- // Similar to LoginComplete but designed for hot reload scenarios
-
- WriteToChat("Mosswart Massacre hot reload initialization started!");
-
- // 1. Initialize settings - CRITICAL first step
- PluginSettings.Initialize();
-
- // 1b. Initialize chest looter system (needs PluginSettings to be ready)
try
{
- chestLooter = new ChestLooter(CoreManager.Current, PluginSettings.Instance.ChestLooterSettings);
- chestLooter.Initialize();
- chestLooter.StatusChanged += (sender, status) =>
+ // WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}");
+
+ if (IsKilledByMeMessage(e.Text))
{
- VVSTabbedMainView.UpdateChestLooterStatus(status);
- };
+ totalKills++;
+ lastKillTime = DateTime.Now;
+ CalculateKillsPerInterval();
+ MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
+ }
+
+ if (IsRareDiscoveryMessage(e.Text, out string rareText))
+ {
+ rareCount++;
+ MainView.UpdateRareCount(rareCount);
+
+ if (RareMetaEnabled)
+ {
+ Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
+ }
+
+ DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
+ }
+ // if (e.Text.EndsWith("!testrare\""))
+ // {
+ // string simulatedText = $"{CoreManager.Current.CharacterFilter.Name} has discovered the Ancient Pickle!";
+ //
+ // if (IsRareDiscoveryMessage(simulatedText, out string simulatedRareText))
+ // {
+ // rareCount++;
+ // MainView.UpdateRareCount(rareCount);
+ //
+ // if (RareMetaEnabled)
+ // {
+ // Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
+ // }
+ //
+ // DelayedCommandManager.AddDelayedCommand($"/a {simulatedRareText}", 3000);
+ // }
+ // else
+ // {
+ // WriteToChat("[Test] Simulated rare message didn't match the regex.");
+ // }
+ //
+ // return;
+ // }
+ if (e.Color == 18 && e.Text.EndsWith("!report\""))
+ {
+ TimeSpan elapsed = DateTime.Now - statsStartTime;
+ string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}";
+ WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
+ MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
+ }
+ if (RemoteCommandsEnabled && e.Color == 18)
+ {
+ string characterName = Regex.Escape(CoreManager.Current.CharacterFilter.Name);
+ string pattern = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!do {characterName} (?.+)\""$";
+ string tag = Regex.Escape(PluginCore.CharTag);
+ string patterntag = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!dot {tag} (?.+)\""$";
+
+
+ var match = Regex.Match(e.Text, pattern);
+ var matchtag = Regex.Match(e.Text, patterntag);
+
+ if (match.Success)
+ {
+ string command = match.Groups["command"].Value;
+ DispatchChatToBoxWithPluginIntercept(command);
+ DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
+ }
+ else if (matchtag.Success)
+ {
+ string command = matchtag.Groups["command"].Value;
+ DispatchChatToBoxWithPluginIntercept(command);
+ DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
+ }
+
+ }
+
+
+
+
+
+
+
+
}
catch (Exception ex)
{
- WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
- }
-
- // 2. Initialize rare tracker if not already set (missed when LoginComplete doesn't fire)
- if (_rareTracker == null)
- {
- _rareTracker = new RareTracker(this);
- _staticRareTracker = _rareTracker;
- _chatEventRouter.SetRareTracker(_rareTracker);
- }
-
- // Apply the values from settings
- _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
- WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
- CharTag = PluginSettings.Instance.CharTag;
-
- // 3. Update UI with current settings and restore window position
- ViewManager.SetRareMetaToggleState(RareMetaEnabled);
- ViewManager.RefreshSettingsFromConfig();
- ViewManager.RestoreWindowPosition();
-
- // 4. Restart services if they were enabled (stop first, then start)
- if (WebSocketEnabled)
- {
- WebSocket.Stop(); // Stop existing
- WebSocket.Start(); // Restart
- }
-
- // 5. Initialize Harmony patches (only if not already done)
- // Note: Harmony patches are global and don't need reinitialization
- if (!DecalHarmonyClean.IsActive())
- {
- try
- {
- bool success = DecalHarmonyClean.Initialize();
- if (success)
- WriteToChat("[OK] Plugin message interception active");
- else
- WriteToChat("[FAIL] Could not initialize message interception");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
- }
- }
-
- // 6. Reinitialize death tracking
- _killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
-
- // 7. Reinitialize cached Prismatic Taper count
- _inventoryMonitor?.Initialize();
-
- // 7b. Reinitialize live inventory tracking
- _liveInventoryTracker?.Initialize();
-
- // 8. Reinitialize quest manager for hot reload
- try
- {
- if (questManager == null)
- {
- questManager = new QuestManager();
- WriteToChat("[OK] Quest manager reinitialized");
- }
- else
- {
- WriteToChat("[INFO] Quest manager already active");
- }
-
- // Trigger full quest data refresh (same as clicking refresh button)
- Views.FlagTrackerView.RefreshQuestData();
- WriteToChat("[INFO] Quest data refresh triggered for hot reload");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Quest manager hot reload failed: {ex.Message}");
- }
-
- // 9. Reinitialize quest streaming service for hot reload
- try
- {
- _questStreamingService?.Stop();
- _questStreamingService = new QuestStreamingService(this);
- _questStreamingService.Start();
-
- WriteToChat("[OK] Quest streaming service reinitialized (30s interval)");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Quest streaming service hot reload failed: {ex.Message}");
- }
-
- // 10. Reinitialize character stats streaming
- try
- {
- if (characterStatsTimer == null)
- {
- characterStatsTimer = new Timer(Constants.CharacterStatsIntervalMs);
- characterStatsTimer.Elapsed += OnCharacterStatsUpdate;
- characterStatsTimer.AutoReset = true;
- characterStatsTimer.Start();
- }
-
- // Send initial stats after short delay
- var initialDelay = new Timer(Constants.LoginDelayMs);
- initialDelay.AutoReset = false;
- initialDelay.Elapsed += (s, args) =>
- {
- CharacterStats.CollectAndSend();
- ((Timer)s).Dispose();
- };
- initialDelay.Start();
-
- WriteToChat("[OK] Character stats streaming initialized");
- }
- catch (Exception ex)
- {
- WriteToChat($"[ERROR] Character stats hot reload failed: {ex.Message}");
- }
-
- WriteToChat("Hot reload initialization completed!");
- }
-
-
- private async void OnSpawn(object sender, CreateObjectEventArgs e)
- {
- var mob = e.New;
- if (mob.ObjectClass != ObjectClass.Monster) return;
-
- try
- {
- // Get DECAL coordinates
- var decalCoords = mob.Coordinates();
- if (decalCoords == null) return;
-
- const string fmt = "F7";
- string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
- string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
-
- // Get Z coordinate using RawCoordinates() for accurate world Z position
- string zCoord = "0";
- try
- {
- var rawCoords = mob.RawCoordinates();
- if (rawCoords != null)
- {
- zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- else
- {
- // Fallback to player Z approximation if RawCoordinates fails
- var playerCoords = Coordinates.Me;
- if (Math.Abs(playerCoords.Z) > 0.1)
- {
- zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- }
- }
- catch
- {
- // Fallback to player Z approximation on error
- try
- {
- var playerCoords = Coordinates.Me;
- if (Math.Abs(playerCoords.Z) > 0.1)
- {
- zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- }
- catch
- {
- zCoord = "0";
- }
- }
-
- await WebSocket.SendSpawnAsync(ns, ew, zCoord, mob.Name);
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[WS] Spawn send failed: {ex}");
+ WriteToChat("Error processing chat message: " + ex.Message);
}
}
-
- private async void OnPortalDetected(object sender, CreateObjectEventArgs e)
- {
- var portal = e.New;
- if (portal.ObjectClass != ObjectClass.Portal) return;
-
- try
- {
- // Get portal coordinates from DECAL
- var decalCoords = portal.Coordinates();
- if (decalCoords == null) return;
-
- const string fmt = "F7";
- string ns = decalCoords.NorthSouth.ToString(fmt, CultureInfo.InvariantCulture);
- string ew = decalCoords.EastWest.ToString(fmt, CultureInfo.InvariantCulture);
-
- // Get Z coordinate using RawCoordinates() for accurate world Z position
- string zCoord = "0";
- try
- {
- var rawCoords = portal.RawCoordinates();
- if (rawCoords != null)
- {
- zCoord = rawCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- else
- {
- // Fallback to player Z approximation if RawCoordinates fails
- var playerCoords = Coordinates.Me;
- if (Math.Abs(playerCoords.Z) > 0.1)
- {
- zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- }
- }
- catch
- {
- // Fallback to player Z approximation on error
- try
- {
- var playerCoords = Coordinates.Me;
- if (Math.Abs(playerCoords.Z) > 0.1)
- {
- zCoord = playerCoords.Z.ToString("F2", CultureInfo.InvariantCulture);
- }
- }
- catch
- {
- zCoord = "0";
- }
- }
-
- await WebSocket.SendPortalAsync(ns, ew, zCoord, portal.Name);
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[PORTAL ERROR] {ex.Message}");
- PluginCore.WriteToChat($"[WS] Portal send failed: {ex}");
- }
- }
-
-
- private void OnDespawn(object sender, ReleaseObjectEventArgs e)
- {
- var mob = e.Released;
- if (mob.ObjectClass != ObjectClass.Monster) return;
-
-
- // var c = mob.Coordinates();
- // PluginCore.WriteToChat(
- // $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})");
- }
-
-
- private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
- {
- _killTracker.OnDeath();
- _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
- }
-
- private void HandleServerCommand(CommandEnvelope env)
- {
- // This is called from WebSocket thread - queue for main thread execution
- lock (pendingCommands)
- {
- pendingCommands.Enqueue(env.Command);
- }
- }
-
- private void ProcessPendingCommands(object sender, EventArgs e)
- {
- // This runs on the main UI thread via Windows Forms timer
- string command = null;
-
- lock (pendingCommands)
- {
- if (pendingCommands.Count > 0)
- command = pendingCommands.Dequeue();
- }
-
- if (command != null)
- {
- try
- {
- // Execute ALL WebSocket commands on main thread - fast and reliable
- DispatchChatToBoxWithPluginIntercept(command);
- }
- catch (Exception ex)
- {
- WriteToChat($"[WS] Command execution error: {ex.Message}");
- }
- }
- }
-
-
-
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
{
try
@@ -892,108 +225,122 @@ namespace MosswartMassacre
}
}
-
- private static void SendVitalsUpdate(object sender, ElapsedEventArgs e)
+ private void UpdateStats(object sender, ElapsedEventArgs e)
{
try
{
- // Only send if WebSocket is enabled
- if (!WebSocketEnabled)
- return;
+ // Update the elapsed time
+ TimeSpan elapsed = DateTime.Now - statsStartTime;
+ MainView.UpdateElapsedTime(elapsed);
- // Collect vitals data
- int currentHealth = CoreManager.Current.Actions.Vital[VitalType.CurrentHealth];
- int currentStamina = CoreManager.Current.Actions.Vital[VitalType.CurrentStamina];
- int currentMana = CoreManager.Current.Actions.Vital[VitalType.CurrentMana];
-
- int maxHealth = CoreManager.Current.Actions.Vital[VitalType.MaximumHealth];
- int maxStamina = CoreManager.Current.Actions.Vital[VitalType.MaximumStamina];
- int maxMana = CoreManager.Current.Actions.Vital[VitalType.MaximumMana];
-
- int vitae = CoreManager.Current.CharacterFilter.Vitae;
-
- // Create vitals data structure
- var vitalsData = new
- {
- type = "vitals",
- timestamp = DateTime.UtcNow.ToString("o"),
- character_name = CoreManager.Current.CharacterFilter.Name,
- health_current = currentHealth,
- health_max = maxHealth,
- health_percentage = maxHealth > 0 ? Math.Round((double)currentHealth / maxHealth * 100, 1) : 0,
- stamina_current = currentStamina,
- stamina_max = maxStamina,
- stamina_percentage = maxStamina > 0 ? Math.Round((double)currentStamina / maxStamina * 100, 1) : 0,
- mana_current = currentMana,
- mana_max = maxMana,
- mana_percentage = maxMana > 0 ? Math.Round((double)currentMana / maxMana * 100, 1) : 0,
- vitae = vitae
- };
-
- // Send via WebSocket
- _ = WebSocket.SendVitalsAsync(vitalsData);
+ // Recalculate kill rates
+ CalculateKillsPerInterval();
+ MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
catch (Exception ex)
{
- WriteToChat($"Error sending vitals: {ex.Message}");
+ WriteToChat("Error updating stats: " + ex.Message);
}
}
- private static void OnCharacterStatsUpdate(object sender, ElapsedEventArgs e)
+ private void CalculateKillsPerInterval()
{
- try
+ double minutesElapsed = (DateTime.Now - statsStartTime).TotalMinutes;
+
+ if (minutesElapsed > 0)
{
- CharacterStats.CollectAndSend();
- }
- catch (Exception ex)
- {
- WriteToChat($"[CharStats] Timer error: {ex.Message}");
+ killsPer5Min = (totalKills / minutesElapsed) * 5;
+ killsPerHour = (totalKills / minutesElapsed) * 60;
}
}
+ private bool IsKilledByMeMessage(string text)
+ {
+ string[] killPatterns = new string[]
+ {
+ @"^You flatten (?.+)'s body with the force of your assault!$",
+ @"^You bring (?.+) to a fiery end!$",
+ @"^You beat (?.+) to a lifeless pulp!$",
+ @"^You smite (?.+) mightily!$",
+ @"^You obliterate (?.+)!$",
+ @"^You run (?.+) through!$",
+ @"^You reduce (?.+) to a sizzling, oozing mass!$",
+ @"^You knock (?.+) into next Morningthaw!$",
+ @"^You split (?.+) apart!$",
+ @"^You cleave (?.+) in twain!$",
+ @"^You slay (?.+) viciously enough to impart death several times over!$",
+ @"^You reduce (?.+) to a drained, twisted corpse!$",
+ @"^Your killing blow nearly turns (?.+) inside-out!$",
+ @"^Your attack stops (?.+) cold!$",
+ @"^Your lightning coruscates over (?.+)'s mortal remains!$",
+ @"^Your assault sends (?.+) to an icy death!$",
+ @"^You killed (?.+)!$",
+ @"^The thunder of crushing (?.+) is followed by the deafening silence of death!$",
+ @"^The deadly force of your attack is so strong that (?.+)'s ancestors feel it!$",
+ @"^(?.+)'s seared corpse smolders before you!$",
+ @"^(?.+) is reduced to cinders!$",
+ @"^(?.+) is shattered by your assault!$",
+ @"^(?.+) catches your attack, with dire consequences!$",
+ @"^(?.+) is utterly destroyed by your attack!$",
+ @"^(?.+) suffers a frozen fate!$",
+ @"^(?.+)'s perforated corpse falls before you!$",
+ @"^(?.+) is fatally punctured!$",
+ @"^(?.+)'s death is preceded by a sharp, stabbing pain!$",
+ @"^(?.+) is torn to ribbons by your assault!$",
+ @"^(?.+) is liquified by your attack!$",
+ @"^(?.+)'s last strength dissolves before you!$",
+ @"^Electricity tears (?.+) apart!$",
+ @"^Blistered by lightning, (?.+) falls!$",
+ @"^(?.+)'s last strength withers before you!$",
+ @"^(?.+) is dessicated by your attack!$",
+ @"^(?.+) is incinerated by your assault!$"
+ };
+ foreach (string pattern in killPatterns)
+ {
+ if (Regex.IsMatch(text, pattern))
+ return true;
+ }
+
+ return false;
+ }
+ private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
+ {
+ rareTextOnly = null;
+
+ // Match pattern: " has discovered the !"
+ string pattern = @"^(?['A-Za-z ]+)\s(?has discovered the .*!$)";
+ Match match = Regex.Match(text, pattern);
+
+ if (match.Success && match.Groups["name"].Value == CoreManager.Current.CharacterFilter.Name)
+ {
+ rareTextOnly = match.Groups["text"].Value; // just "has discovered the Ancient Pickle!"
+ return true;
+ }
+
+ return false;
+ }
public static void WriteToChat(string message)
{
- try
- {
- // For hot reload scenarios where MyHost might be null, use CoreManager directly
- if (MyHost != null)
- {
- MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
- }
- else
- {
- // Hot reload fallback1 - use CoreManager directly like the original template
- CoreManager.Current.Actions.AddChatText("[Mosswart Massacre] " + message, 1);
- }
- }
- catch (Exception ex)
- {
- // Last resort fallback - try CoreManager even if MyHost was supposed to work
- try
- {
- CoreManager.Current.Actions.AddChatText($"[Mosswart Massacre] {message} (WriteToChat error: {ex.Message})", 1);
- }
- catch
- {
- // Give up - can't write to chat at all
- }
- }
+ MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
}
-
- void IPluginLogger.Log(string message) => WriteToChat(message);
-
public static void RestartStats()
{
- _staticKillTracker?.RestartStats();
- if (_staticRareTracker != null)
- _staticRareTracker.RareCount = 0;
- ViewManager.UpdateRareCount(0);
+ totalKills = 0;
+ rareCount = 0;
+ statsStartTime = DateTime.Now;
+ killsPer5Min = 0;
+ killsPerHour = 0;
+
+ WriteToChat("Stats have been reset.");
+ MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
+ MainView.UpdateRareCount(rareCount);
}
public static void ToggleRareMeta()
{
- _staticRareTracker?.ToggleRareMeta();
- ViewManager.SetRareMetaToggleState(RareMetaEnabled);
+ PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
+ RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ MainView.SetRareMetaToggleState(RareMetaEnabled);
}
[DllImport("Decal.dll")]
@@ -1020,512 +367,138 @@ namespace MosswartMassacre
}
private void HandleMmCommand(string text)
{
- _commandRouter.Dispatch(text);
- }
+ // Remove the /mm prefix and trim extra whitespace
+ string[] args = text.Substring(3).Trim().Split(' ');
- private void RegisterCommands()
- {
- _commandRouter.Register("ws", args =>
+ if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
{
- if (args.Length > 1)
- {
- if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
+ WriteToChat("Usage: /mm . Try /mm help");
+ return;
+ }
+
+ string subCommand = args[0].ToLower();
+
+ switch (subCommand)
+ {
+ case "telemetry":
+ if (args.Length > 1)
{
- WebSocketEnabled = true;
- WebSocket.Start();
- PluginSettings.Instance.WebSocketEnabled = true;
- WriteToChat("WS streaming ENABLED.");
- }
- else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
- {
- WebSocketEnabled = false;
- WebSocket.Stop();
- PluginSettings.Instance.WebSocketEnabled = false;
- WriteToChat("WS streaming DISABLED.");
- }
- else
- {
- WriteToChat("Usage: /mm ws ");
- }
- }
- else
- {
- WriteToChat("Usage: /mm ws ");
- }
- }, "Enable/disable WebSocket streaming");
-
- _commandRouter.Register("report", args =>
- {
- TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
- string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}, Session Deaths: {_killTracker.SessionDeaths}, Total Deaths: {_killTracker.TotalDeaths}";
- WriteToChat(reportMessage);
- }, "Show kill/death/rare stats");
-
- _commandRouter.Register("getmetastate", args =>
- {
- string metaState = VtankControl.VtGetMetaState();
- WriteToChat(metaState);
- }, "Show current VTank meta state");
-
- _commandRouter.Register("loc", args =>
- {
- Coordinates here = Coordinates.Me;
- var pos = Utils.GetPlayerPosition();
- WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
- }, "Show current location");
-
- _commandRouter.Register("reset", args =>
- {
- RestartStats();
- }, "Reset kill/rare counters");
-
- _commandRouter.Register("meta", args =>
- {
- RareMetaEnabled = !RareMetaEnabled;
- WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
- ViewManager.SetRareMetaToggleState(RareMetaEnabled);
- }, "Toggle rare meta state");
-
- _commandRouter.Register("nextwp", args =>
- {
- double result = VtankControl.VtAdvanceWaypoint();
- if (result == 1)
- WriteToChat("Advanced VTank to next waypoint.");
- else
- WriteToChat("Failed to advance VTank waypoint. Is VTank running?");
- }, "Advance VTank to next waypoint");
-
- _commandRouter.Register("setchest", args =>
- {
- if (args.Length < 2)
- {
- WriteToChat("[ChestLooter] Usage: /mm setchest ");
- return;
- }
- string chestName = string.Join(" ", args.Skip(1));
- if (chestLooter != null)
- {
- chestLooter.SetChestName(chestName);
- if (PluginSettings.Instance?.ChestLooterSettings != null)
- {
- PluginSettings.Instance.ChestLooterSettings.ChestName = chestName;
- PluginSettings.Save();
- }
- Views.VVSTabbedMainView.RefreshChestLooterUI();
- }
- }, "Set chest name for looter");
-
- _commandRouter.Register("setkey", args =>
- {
- if (args.Length < 2)
- {
- WriteToChat("[ChestLooter] Usage: /mm setkey ");
- return;
- }
- string keyName = string.Join(" ", args.Skip(1));
- if (chestLooter != null)
- {
- chestLooter.SetKeyName(keyName);
- if (PluginSettings.Instance?.ChestLooterSettings != null)
- {
- PluginSettings.Instance.ChestLooterSettings.KeyName = keyName;
- PluginSettings.Save();
- }
- Views.VVSTabbedMainView.RefreshChestLooterUI();
- }
- }, "Set key name for looter");
-
- _commandRouter.Register("lootchest", args =>
- {
- if (chestLooter != null)
- {
- if (!chestLooter.StartByName())
- WriteToChat("[ChestLooter] Failed to start. Check chest/key names are set.");
- }
- else
- {
- WriteToChat("[ChestLooter] Chest looter not initialized");
- }
- }, "Start chest looting");
-
- _commandRouter.Register("stoploot", args =>
- {
- if (chestLooter != null)
- chestLooter.Stop();
- else
- WriteToChat("[ChestLooter] Chest looter not initialized");
- }, "Stop chest looting");
-
- _commandRouter.Register("vtanktest", args =>
- {
- try
- {
- WriteToChat("Testing VTank interface...");
- WriteToChat($"VTank Instance: {(vTank.Instance != null ? "Found" : "NULL")}");
- WriteToChat($"VTank Type: {vTank.Instance?.GetType()?.Name ?? "NULL"}");
- WriteToChat($"NavCurrent: {vTank.Instance?.NavCurrent ?? -1}");
- WriteToChat($"NavNumPoints: {vTank.Instance?.NavNumPoints ?? -1}");
- WriteToChat($"NavType: {vTank.Instance?.NavType}");
- WriteToChat($"MacroEnabled: {vTank.Instance?.MacroEnabled}");
- }
- catch (Exception ex)
- {
- WriteToChat($"VTank test error: {ex.Message}");
- }
- }, "");
-
- _commandRouter.Register("decalstatus", args =>
- {
- try
- {
- WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
- WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
- WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
- WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
-
- WriteToChat("=== Harmony Version Status ===");
- try
- {
- var harmonyTest = Harmony.HarmonyInstance.Create("test.version.check");
- WriteToChat($"[OK] Harmony Available (ID: {harmonyTest.Id})");
- var harmonyAssembly = typeof(Harmony.HarmonyInstance).Assembly;
- WriteToChat($"[OK] Harmony Version: {harmonyAssembly.GetName().Version}");
- WriteToChat($"[OK] Harmony Location: {harmonyAssembly.Location}");
- }
- catch (Exception harmonyEx)
- {
- WriteToChat($"[FAIL] Harmony Test Failed: {harmonyEx.Message}");
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"Status check error: {ex.Message}");
- }
- }, "");
-
- _commandRouter.Register("decaldebug", args =>
- {
- if (args.Length > 1)
- {
- if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
- {
- AggressiveChatStreamingEnabled = true;
- WriteToChat("[OK] DECAL debug streaming ENABLED - will show captured messages + stream via WebSocket");
- }
- else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
- {
- AggressiveChatStreamingEnabled = false;
- WriteToChat("[FAIL] DECAL debug streaming DISABLED - WebSocket streaming also disabled");
- }
- else
- {
- WriteToChat("Usage: /mm decaldebug ");
- }
- }
- else
- {
- WriteToChat("Usage: /mm decaldebug ");
- }
- }, "");
-
- _commandRouter.Register("gui", args =>
- {
- try
- {
- WriteToChat("Attempting to manually initialize GUI...");
- ViewManager.ViewDestroy();
- ViewManager.ViewInit();
- WriteToChat("GUI initialization attempt completed.");
- }
- catch (Exception ex)
- {
- WriteToChat($"GUI initialization error: {ex.Message}");
- }
- }, "Reinitialize GUI");
-
- _commandRouter.Register("testprismatic", args =>
- {
- try
- {
- WriteToChat("=== FULL INVENTORY DUMP ===");
- var worldFilter = CoreManager.Current.WorldFilter;
- var playerInv = CoreManager.Current.CharacterFilter.Id;
-
- WriteToChat("Listing ALL items in your main inventory:");
- int itemNum = 1;
-
- foreach (WorldObject item in worldFilter.GetByContainer(playerInv))
- {
- if (!string.IsNullOrEmpty(item.Name))
+ if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
- int stackCount = item.Values(LongValueKey.StackCount, 0);
- WriteToChat($"{itemNum:D2}: '{item.Name}' (count: {stackCount}, icon: 0x{item.Icon:X}, class: {item.ObjectClass})");
- itemNum++;
-
- string nameLower = item.Name.ToLower();
- if (nameLower.Contains("taper") || nameLower.Contains("prismatic") ||
- nameLower.Contains("prism") || nameLower.Contains("component"))
- {
- WriteToChat($" *** POSSIBLE MATCH: '{item.Name}' ***");
- }
+ TelemetryEnabled = true;
+ Telemetry.Start();
+ PluginSettings.Instance.TelemetryEnabled = true;
+ WriteToChat("Telemetry streaming ENABLED.");
+ }
+ else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
+ {
+ TelemetryEnabled = false;
+ Telemetry.Stop();
+ PluginSettings.Instance.TelemetryEnabled = false;
+ WriteToChat("Telemetry streaming DISABLED.");
+ }
+ else
+ {
+ WriteToChat("Usage: /mm telemetry ");
}
}
-
- WriteToChat($"=== Total items listed: {itemNum - 1} ===");
-
- WriteToChat("=== Testing Utility Functions on Prismatic Taper ===");
- var foundItem = Utils.FindItemByName("Prismatic Taper");
- if (foundItem != null)
- {
- WriteToChat($"SUCCESS! Found: '{foundItem.Name}'");
- WriteToChat($"Utils.GetItemStackSize: {Utils.GetItemStackSize("Prismatic Taper")}");
- WriteToChat($"Utils.GetItemIcon: 0x{Utils.GetItemIcon("Prismatic Taper"):X}");
- WriteToChat($"Utils.GetItemDisplayIcon: 0x{Utils.GetItemDisplayIcon("Prismatic Taper"):X}");
- WriteToChat("=== TELEMETRY WILL NOW WORK! ===");
- }
else
{
- WriteToChat("ERROR: Still can't find Prismatic Taper with utility functions!");
+ WriteToChat("Usage: /mm telemetry ");
}
- }
- catch (Exception ex)
- {
- WriteToChat($"Search error: {ex.Message}");
- }
- }, "");
+ break;
+ case "help":
+ WriteToChat("Mosswart Massacre Commands:");
+ WriteToChat("/mm report - Show current stats");
+ WriteToChat("/mm loc - Show current location");
+ WriteToChat("/mm telemetry - Telemetry streaming enable|disable"); // NEW
+ WriteToChat("/mm reset - Reset all counters");
+ WriteToChat("/mm meta - Toggle rare meta state");
+ WriteToChat("/mm http - Local http-command server enable|disable");
+ WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable");
+ WriteToChat("/mm getmetastate - Gets the current metastate");
+ break;
- _commandRouter.Register("deathstats", args =>
- {
- try
- {
- WriteToChat("=== Death Tracking Statistics ===");
- WriteToChat($"Session Deaths: {_killTracker.SessionDeaths}");
- WriteToChat($"Total Deaths: {_killTracker.TotalDeaths}");
+ case "report":
+ TimeSpan elapsed = DateTime.Now - statsStartTime;
+ string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}";
+ WriteToChat(reportMessage);
+ break;
+ case "getmetastate":
+ string metaState = VtankControl.VtGetMetaState();
+ WriteToChat(metaState);
+ break;
- int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
-
- if (currentCharDeaths != _killTracker.TotalDeaths)
+ case "loc":
+ Coordinates here = Coordinates.Me;
+ var pos = Utils.GetPlayerPosition();
+ WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
+ break;
+ case "reset":
+ RestartStats();
+ break;
+ case "meta":
+ RareMetaEnabled = !RareMetaEnabled;
+ WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
+ MainView.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
+ break;
+
+ case "http":
+ if (args.Length > 1)
{
- WriteToChat($"[WARNING] Death count sync issue detected!");
- WriteToChat($"Updating totalDeaths from {_killTracker.TotalDeaths} to {currentCharDeaths}");
- _killTracker.SetTotalDeaths(currentCharDeaths);
- }
-
- WriteToChat("Death tracking is active and will increment on character death.");
- }
- catch (Exception ex)
- {
- WriteToChat($"Death stats error: {ex.Message}");
- }
- }, "Show death tracking stats");
-
- _commandRouter.Register("testdeath", args =>
- {
- try
- {
- WriteToChat("=== Manual Death Test ===");
- WriteToChat($"Current sessionDeaths variable: {_killTracker.SessionDeaths}");
- WriteToChat($"Current totalDeaths variable: {_killTracker.TotalDeaths}");
-
- int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
- WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
-
- _killTracker.OnDeath();
- WriteToChat($"Manually incremented sessionDeaths to: {_killTracker.SessionDeaths}");
- WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
-
- WriteToChat($"Death event subscription check:");
- var deathEvent = typeof(Decal.Adapter.Wrappers.CharacterFilter).GetEvent("Death");
- WriteToChat($"Death event exists: {deathEvent != null}");
- }
- catch (Exception ex)
- {
- WriteToChat($"Test death error: {ex.Message}");
- }
- }, "");
-
- _commandRouter.Register("testtaper", args =>
- {
- try
- {
- WriteToChat("=== Cached Taper Tracking Test ===");
- WriteToChat($"Cached Count: {_inventoryMonitor.CachedPrismaticCount}");
- WriteToChat($"Last Count: {_inventoryMonitor.LastPrismaticCount}");
-
- int utilsCount = Utils.GetItemStackSize("Prismatic Taper");
- WriteToChat($"Utils Count: {utilsCount}");
-
- if (_inventoryMonitor.CachedPrismaticCount == utilsCount)
- {
- WriteToChat("[OK] Cached count matches Utils count");
- }
- else
- {
- WriteToChat($"[WARNING] Count mismatch! Cached: {_inventoryMonitor.CachedPrismaticCount}, Utils: {utilsCount}");
- WriteToChat("Refreshing cached count...");
- _inventoryMonitor.Initialize();
- }
-
- WriteToChat("=== Container Analysis ===");
- int mainPackCount = 0;
- int sidePackCount = 0;
- int playerId = CoreManager.Current.CharacterFilter.Id;
-
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
+ if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
- int stackCount = wo.Values(LongValueKey.StackCount, 1);
- if (wo.Container == playerId)
- mainPackCount += stackCount;
- else
- sidePackCount += stackCount;
+ PluginSettings.Instance.HttpServerEnabled = true;
+ HttpServerEnabled = true;
+ HttpCommandServer.Start();
+ }
+ else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
+ {
+ PluginSettings.Instance.HttpServerEnabled = false;
+ HttpServerEnabled = false;
+ HttpCommandServer.Stop();
+ }
+ else
+ {
+ WriteToChat("Usage: /mm http ");
}
}
-
- WriteToChat($"Main Pack Tapers: {mainPackCount}");
- WriteToChat($"Side Pack Tapers: {sidePackCount}");
- WriteToChat($"Total: {mainPackCount + sidePackCount}");
-
- WriteToChat("=== Event System Status ===");
- WriteToChat($"Tracking {_inventoryMonitor.TrackedTaperCount} taper stacks for delta detection");
- WriteToChat($"Known stack sizes: {_inventoryMonitor.KnownStackSizesCount} items");
- WriteToChat("Pure delta tracking - NO expensive inventory scans during events!");
- WriteToChat("Now tracks: consumption, drops, trades, container moves");
- WriteToChat("Try moving tapers between containers and casting spells!");
- }
- catch (Exception ex)
- {
- WriteToChat($"Taper test error: {ex.Message}");
- }
- }, "");
-
- _commandRouter.Register("finditem", args =>
- {
- if (args.Length > 1)
- {
- string itemName = string.Join(" ", args, 1, args.Length - 1).Trim('"');
- WriteToChat($"=== Searching for: '{itemName}' ===");
-
- var foundItem = Utils.FindItemByName(itemName);
- if (foundItem != null)
+ else
{
- WriteToChat($"FOUND: '{foundItem.Name}'");
- WriteToChat($"Count: {foundItem.Values(LongValueKey.StackCount, 0)}");
- WriteToChat($"Icon: 0x{foundItem.Icon:X}");
- WriteToChat($"Display Icon: 0x{(foundItem.Icon + 0x6000000):X}");
- WriteToChat($"Object Class: {foundItem.ObjectClass}");
+ WriteToChat("Usage: /mm http ");
+ }
+ break;
+
+ case "remotecommands":
+ if (args.Length > 1)
+ {
+ if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
+ {
+ PluginSettings.Instance.RemoteCommandsEnabled = true;
+ RemoteCommandsEnabled = true;
+ WriteToChat("Remote command listening is now ENABLED.");
+ }
+ else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
+ {
+ PluginSettings.Instance.RemoteCommandsEnabled = false;
+ RemoteCommandsEnabled = false;
+ WriteToChat("Remote command listening is now DISABLED.");
+ }
+ else
+ {
+ WriteToChat("Invalid remotecommands argument. Use 'enable' or 'disable'.");
+ }
}
else
{
- WriteToChat($"NOT FOUND: '{itemName}'");
- WriteToChat("Make sure the name is exactly as it appears in-game.");
+ WriteToChat("Usage: /mm remotecommands ");
}
- }
- else
- {
- WriteToChat("Usage: /mm finditem \"Item Name\"");
- WriteToChat("Example: /mm finditem \"Prismatic Taper\"");
- }
- }, "Find item in inventory");
+ break;
- _commandRouter.Register("checkforupdate", args =>
- {
- Task.Run(async () =>
- {
- await UpdateManager.CheckForUpdateAsync();
- try
- {
- ViewManager.RefreshUpdateStatus();
- }
- catch (Exception ex)
- {
- WriteToChat($"Error refreshing UI: {ex.Message}");
- }
- });
- }, "Check for plugin updates");
-
- _commandRouter.Register("update", args =>
- {
- Task.Run(async () =>
- {
- await UpdateManager.DownloadAndInstallUpdateAsync();
- });
- }, "Download and install latest update");
-
- _commandRouter.Register("debugupdate", args =>
- {
- Views.VVSTabbedMainView.DebugUpdateControls();
- }, "");
-
- _commandRouter.Register("sendinventory", args =>
- {
- if (_inventoryLogger != null)
- _inventoryLogger.ForceInventoryUpload();
- else
- WriteToChat("[INV] Inventory system not initialized");
- }, "Force full inventory upload");
-
- _commandRouter.Register("refreshquests", args =>
- {
- try
- {
- WriteToChat("[QUEST] Refreshing quest data...");
- Views.FlagTrackerView.RefreshQuestData();
- }
- catch (Exception ex)
- {
- WriteToChat($"[QUEST] Refresh failed: {ex.Message}");
- }
- }, "Refresh quest data");
-
- _commandRouter.Register("queststatus", args =>
- {
- try
- {
- WriteToChat("=== Quest Streaming Status ===");
- WriteToChat($"Timer Active: {_questStreamingService?.IsRunning ?? false}");
- WriteToChat($"WebSocket Enabled: {WebSocketEnabled}");
- WriteToChat($"Quest Manager: {(questManager != null ? "Active" : "Not Active")}");
- WriteToChat($"Quest Count: {questManager?.QuestList?.Count ?? 0}");
-
- if (questManager?.QuestList != null)
- {
- var priorityQuests = questManager.QuestList
- .Where(q => QuestStreamingService.IsHighPriorityQuest(q.Id))
- .GroupBy(q => q.Id)
- .Select(g => g.First())
- .ToList();
- WriteToChat($"Priority Quests Found: {priorityQuests.Count}");
- foreach (var quest in priorityQuests)
- {
- string questName = questManager.GetFriendlyQuestName(quest.Id);
- WriteToChat($" - {questName} ({quest.Id})");
- }
- }
-
- WriteToChat($"Verbose Logging: {PluginSettings.Instance?.VerboseLogging ?? false}");
- WriteToChat("Use '/mm verbose' to toggle debug logging");
- }
- catch (Exception ex)
- {
- WriteToChat($"[QUEST] Status check failed: {ex.Message}");
- }
- }, "Show quest streaming status");
-
- _commandRouter.Register("verbose", args =>
- {
- if (PluginSettings.Instance != null)
- {
- PluginSettings.Instance.VerboseLogging = !PluginSettings.Instance.VerboseLogging;
- WriteToChat($"Verbose logging: {(PluginSettings.Instance.VerboseLogging ? "ENABLED" : "DISABLED")}");
- }
- else
- {
- WriteToChat("Settings not initialized");
- }
- }, "Toggle verbose debug logging");
+ default:
+ WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
+ break;
+ }
}
diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs
index 8104493..e59f09e 100644
--- a/MosswartMassacre/PluginSettings.cs
+++ b/MosswartMassacre/PluginSettings.cs
@@ -13,60 +13,25 @@ namespace MosswartMassacre
private static readonly object _sync = new object();
// backing fields
+ private bool _remoteCommandsEnabled = false;
private bool _rareMetaEnabled = true;
- private bool _webSocketEnabled = false;
- private bool _inventorylog = true;
+ private bool _httpServerEnabled = false;
+ private bool _telemetryEnabled = false;
private string _charTag = "default";
- private int _mainWindowX = 100;
- private int _mainWindowY = 100;
- private bool _useTabbedInterface = true;
- private string _vtankProfilesPath = "";
- private bool _verboseLogging = false;
- private ChestLooterSettings _chestLooterSettings = new ChestLooterSettings();
public static PluginSettings Instance => _instance
?? throw new InvalidOperationException("PluginSettings not initialized");
public static void Initialize()
{
- // determine plugin folder and character-specific folder
+ // determine settings file path
string characterName = CoreManager.Current.CharacterFilter.Name;
-
- // For hot reload scenarios, use the AssemblyDirectory set by the Loader
- // For normal loading, fall back to the executing assembly location
- string pluginFolder;
- if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
- {
- pluginFolder = PluginCore.AssemblyDirectory;
- }
- else
- {
- pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
- }
-
- // Path for character-specific folder
- string characterFolder = Path.Combine(pluginFolder, characterName);
-
- // Create the character folder if it doesn't exist
- if (!Directory.Exists(characterFolder))
- {
- try
- {
- Directory.CreateDirectory(characterFolder);
- }
- catch (Exception ex)
- {
- PluginCore.DispatchChatToBoxWithPluginIntercept($"[Settings] Failed to create character folder: {ex.Message}");
- }
- }
-
- // YAML file is now in the character-specific folder
- _filePath = Path.Combine(characterFolder, $"{characterName}.yaml");
+ string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
+ _filePath = Path.Combine(pluginFolder, $"{characterName}.yaml");
// build serializer/deserializer once
var builder = new DeserializerBuilder()
- .WithNamingConvention(UnderscoredNamingConvention.Instance)
- .IgnoreUnmatchedProperties();
+ .WithNamingConvention(UnderscoredNamingConvention.Instance);
var deserializer = builder.Build();
PluginSettings loaded = null;
@@ -135,73 +100,34 @@ namespace MosswartMassacre
}
// public properties
+ public bool RemoteCommandsEnabled
+ {
+ get => _remoteCommandsEnabled;
+ set { _remoteCommandsEnabled = value; Save(); }
+ }
+
public bool RareMetaEnabled
{
get => _rareMetaEnabled;
set { _rareMetaEnabled = value; Save(); }
}
- public bool WebSocketEnabled
+ public bool HttpServerEnabled
{
- get => _webSocketEnabled;
- set { _webSocketEnabled = value; Save(); }
+ get => _httpServerEnabled;
+ set { _httpServerEnabled = value; Save(); }
}
+
+ public bool TelemetryEnabled
+ {
+ get => _telemetryEnabled;
+ set { _telemetryEnabled = value; Save(); }
+ }
+
public string CharTag
{
get => _charTag;
set { _charTag = value; Save(); }
}
- public bool InventoryLog
- {
- get => _inventorylog;
- set { _inventorylog = value; Save(); }
- }
-
- public int MainWindowX
- {
- get => _mainWindowX;
- set { _mainWindowX = value; Save(); }
- }
-
- public int MainWindowY
- {
- get => _mainWindowY;
- set { _mainWindowY = value; Save(); }
- }
-
- public bool UseTabbedInterface
- {
- get => _useTabbedInterface;
- set { _useTabbedInterface = value; Save(); }
- }
-
- public string VTankProfilesPath
- {
- get => _vtankProfilesPath;
- set { _vtankProfilesPath = value; Save(); }
- }
-
- public bool VerboseLogging
- {
- get => _verboseLogging;
- set { _verboseLogging = value; Save(); }
- }
-
- public ChestLooterSettings ChestLooterSettings
- {
- get
- {
- if (_chestLooterSettings == null)
- {
- _chestLooterSettings = new ChestLooterSettings();
- }
- return _chestLooterSettings;
- }
- set
- {
- _chestLooterSettings = value;
- Save();
- }
- }
}
}
diff --git a/MosswartMassacre/Properties/AssemblyInfo.cs b/MosswartMassacre/Properties/AssemblyInfo.cs
index 216b745..744091c 100644
--- a/MosswartMassacre/Properties/AssemblyInfo.cs
+++ b/MosswartMassacre/Properties/AssemblyInfo.cs
@@ -21,4 +21,10 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9b6a07e1-ae78-47f4-b09c-174f6a27d7a3")]
-// Version is auto-generated at build time (CalVer: YYYY.M.D.HHmm)
\ No newline at end of file
+// Version information for an assembly consists of the following four values:
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+[assembly: AssemblyVersion("2.0.0.0")]
+[assembly: AssemblyFileVersion("2.0.0.0")]
\ No newline at end of file
diff --git a/MosswartMassacre/QuestManager.cs b/MosswartMassacre/QuestManager.cs
deleted file mode 100644
index a357e51..0000000
--- a/MosswartMassacre/QuestManager.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using Decal.Adapter;
-using Decal.Adapter.Wrappers;
-
-namespace MosswartMassacre
-{
- ///
- /// Quest tracking and management system
- /// Ported from UBS Lua quest system
- ///
- public class QuestManager : IDisposable
- {
- #region Quest Data Structures
- public class Quest
- {
- public string Id { get; set; }
- public int Solves { get; set; }
- public int Timestamp { get; set; }
- public string Description { get; set; }
- public int MaxSolves { get; set; }
- public int Delta { get; set; }
- public int ExpireTime { get; set; }
- }
- #endregion
-
- #region Properties
- public List QuestList { get; private set; }
- public Dictionary QuestDictionary { get; private set; }
- #endregion
-
- #region Events and State
- private bool isRefreshing = false;
- private DateTime lastRefreshTime = DateTime.MinValue;
- #endregion
-
- public QuestManager()
- {
- QuestList = new List();
- QuestDictionary = new Dictionary();
-
- // Hook into chat events for quest parsing
- InitializeChatHooks();
- }
-
- #region Initialization
- private void InitializeChatHooks()
- {
- try
- {
- if (CoreManager.Current != null)
- {
- CoreManager.Current.ChatBoxMessage += OnChatBoxMessage;
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error initializing quest chat hooks: {ex.Message}");
- }
- }
- #endregion
-
- #region Quest Name Mapping
- public string GetFriendlyQuestName(string questStamp)
- {
- return QuestNames.GetFriendlyName(questStamp);
- }
-
- public string GetQuestDisplayName(string questStamp)
- {
- return QuestNames.GetDisplayName(questStamp);
- }
-
- public int GetQuestNameMappingsCount()
- {
- return QuestNames.QuestStampToName.Count;
- }
- #endregion
-
- #region Quest Parsing
- private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e)
- {
- try
- {
- if (!isRefreshing || string.IsNullOrEmpty(e.Text))
- return;
-
- // Parse quest information from /myquests output
- ParseQuestLine(e.Text);
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error parsing quest line: {ex.Message}");
- }
- }
-
- private void ParseQuestLine(string text)
- {
- try
- {
- // Quest line format: TaskName - Solves solves (Timestamp)"Description" MaxSolves Delta
- // Example: "SomeQuest - 5 solves (1640995200)"Quest description here" 10 3600
- var pattern = @"([^\-]+) - (\d+) solves \((\d+)\)""([^""]+)"" (-?\d+) (\d+)";
- var match = Regex.Match(text, pattern);
-
- if (match.Success)
- {
- var quest = new Quest
- {
- Id = match.Groups[1].Value.Trim(),
- Solves = int.Parse(match.Groups[2].Value),
- Timestamp = int.Parse(match.Groups[3].Value),
- Description = match.Groups[4].Value,
- MaxSolves = int.Parse(match.Groups[5].Value),
- Delta = int.Parse(match.Groups[6].Value)
- };
-
- quest.ExpireTime = quest.Timestamp + quest.Delta;
-
- // Add to collections
- QuestList.Add(quest);
- QuestDictionary[quest.Id] = quest;
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error parsing quest line '{text}': {ex.Message}");
- }
- }
- #endregion
-
- #region Quest Management
- public void RefreshQuests()
- {
- try
- {
- if (isRefreshing)
- return;
-
- ClearQuests();
- isRefreshing = true;
-
- // Issue /myquests command to refresh quest data
- CoreManager.Current.Actions.InvokeChatParser("/myquests");
-
- // Stop listening after a delay
- System.Threading.Timer stopTimer = null;
- stopTimer = new System.Threading.Timer(_ =>
- {
- isRefreshing = false;
- stopTimer?.Dispose();
- lastRefreshTime = DateTime.Now;
- }, null, 3000, System.Threading.Timeout.Infinite);
- }
- catch (Exception ex)
- {
- isRefreshing = false;
- PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}");
- }
- }
-
- public void ClearQuests()
- {
- QuestList.Clear();
- QuestDictionary.Clear();
- }
-
- public bool IsQuestAvailable(string questStamp)
- {
- if (!QuestDictionary.TryGetValue(questStamp, out Quest quest))
- return true; // If quest not found, assume available
-
- var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- return quest.ExpireTime < currentTime;
- }
-
- public bool IsQuestMaxSolved(string questStamp)
- {
- if (!QuestDictionary.TryGetValue(questStamp, out Quest quest))
- return false;
-
- return quest.Solves >= quest.MaxSolves;
- }
-
- public bool HasQuestFlag(string questStamp)
- {
- return QuestDictionary.ContainsKey(questStamp);
- }
-
- public string GetTimeUntilExpire(Quest quest)
- {
- if (quest == null)
- return "Unknown";
-
- var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- var timeLeft = quest.ExpireTime - currentTime;
-
- if (timeLeft <= 0)
- return "Ready";
-
- return FormatSeconds((int)timeLeft);
- }
-
- public string FormatTimeStamp(int timestamp)
- {
- try
- {
- var dateTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
- return dateTime.ToString("MM/dd/yyyy HH:mm:ss");
- }
- catch
- {
- return "Invalid";
- }
- }
-
- public string FormatSeconds(int seconds)
- {
- if (seconds <= 0)
- return "0s";
-
- var days = seconds / 86400;
- seconds %= 86400;
- var hours = seconds / 3600;
- seconds %= 3600;
- var minutes = seconds / 60;
- seconds %= 60;
-
- var result = "";
- if (days > 0) result += $"{days}d ";
- if (hours > 0) result += $"{hours}h ";
- if (minutes > 0) result += $"{minutes}m ";
- if (seconds > 0 || string.IsNullOrEmpty(result)) result += $"{seconds}s";
-
- return result.Trim();
- }
-
- public object GetFieldByID(Quest quest, int id)
- {
- if (quest == null)
- return null;
-
- switch (id)
- {
- case 1: return quest.Id;
- case 2: return quest.Solves;
- case 3: return quest.Timestamp;
- case 4: return quest.MaxSolves;
- case 5: return quest.Delta;
- case 6: return quest.ExpireTime;
- default: return quest.Id;
- }
- }
- #endregion
-
- #region Society Quest Helpers
- public string GetSocietyName(int factionBits)
- {
- switch (factionBits)
- {
- case 1: return "Celestial Hand";
- case 2: return "Eldrytch Web";
- case 4: return "Radiant Blood";
- default: return "Unknown";
- }
- }
-
- public string GetSocietyRank(int ribbons)
- {
- if (ribbons >= 1001) return "Master";
- if (ribbons >= 601) return "Lord";
- if (ribbons >= 301) return "Knight";
- if (ribbons >= 101) return "Adept";
- if (ribbons >= 1) return "Initiate";
- return "None";
- }
-
- public int GetMaxRibbonsPerDay(string rank)
- {
- switch (rank)
- {
- case "Initiate": return 50;
- case "Adept": return 100;
- case "Knight": return 150;
- case "Lord": return 200;
- case "Master": return 250;
- default: return 0;
- }
- }
- #endregion
-
- #region Cleanup
- public void Dispose()
- {
- try
- {
- if (CoreManager.Current != null)
- {
- CoreManager.Current.ChatBoxMessage -= OnChatBoxMessage;
- }
-
- ClearQuests();
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"Error disposing quest manager: {ex.Message}");
- }
- }
- #endregion
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/QuestNames.cs b/MosswartMassacre/QuestNames.cs
deleted file mode 100644
index 9c782b8..0000000
--- a/MosswartMassacre/QuestNames.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-using System.Collections.Generic;
-
-namespace MosswartMassacre
-{
- ///
- /// Static quest name mappings from quest stamp to friendly display name
- /// Based on questtracker repository data
- ///
- public static class QuestNames
- {
- ///
- /// Dictionary mapping quest stamps to friendly quest names
- ///
- public static readonly Dictionary QuestStampToName = new Dictionary
- {
- // Character-specific Quest Stamps (from actual /myquests output)
- ["30minattributes"] = "30 Minute Attribute Gems Timer",
- ["academeyexittokengiven"] = "Academy Exit Token Received",
- ["aerbaxchestkey2pickup"] = "Aerbax Chest Key #2 Pickup",
- ["anekshaygemofknowledgetimer_monthly"] = "A'nekshay Gem of Knowledge Monthly Timer",
- ["anekshaygemoflesserknowledgecollectedinamonth"] = "A'nekshay Gems of Lesser Knowledge Monthly Count",
- ["anekshaygemoflesserknowledgetimer_monthly"] = "A'nekshay Gem of Lesser Knowledge Monthly Timer",
- ["attributereset30day"] = "30-Day Attribute Reset Timer",
- ["augmentationblankgemacquired"] = "Blank Augmentation Gem Pickup Timer",
- ["bellowsnewbieturnedin"] = "Blacksmith's Bellows Turned In",
- ["bonecrunchkeypickuptimer"] = "Bonecrunch's Key Pickup Timer",
- ["callingstonegiven"] = "Calling Stone Turned Over",
- ["defeatedbonecrunch"] = "Bonecrunch Defeated",
- ["efmlcentermanafieldused"] = "EF Middle Level Center Mana Field Used",
- ["efmleastmanafieldused"] = "EF Middle Level East Mana Field Used",
- ["efmlnorthmanafieldused"] = "EF Middle Level North Mana Field Used",
- ["efmlsouthmanafieldused"] = "EF Middle Level South Mana Field Used",
- ["efmlwestmanafieldused"] = "EF Middle Level West Mana Field Used",
- ["efulcentermanafieldused"] = "EF Upper Level Center Mana Field Used",
- ["efuleastmanafieldused"] = "EF Upper Level East Mana Field Used",
- ["efulnorthmanafieldused"] = "EF Upper Level North Mana Field Used",
- ["efulsouthmanafieldused"] = "EF Upper Level South Mana Field Used",
- ["efulwestmanafieldused"] = "EF Upper Level West Mana Field Used",
- ["insatiableeaterjaw"] = "Insatiable Eater Jaw Collection",
- ["pathwardencomplete"] = "Pathwarden Visit Complete",
- ["pathwardenfound1111"] = "Pathwarden Greeter Encountered",
- ["recallsingularitycaul"] = "Recall Singularity Bore Pickup",
- ["stipendscollectedinamonth"] = "Monthly Stipends Collected Count",
- ["stipendtimer_0812"] = "Stipend Collection Timer",
- ["stipendtimer_monthly"] = "Monthly Stipend Timer",
- ["upperinsatiablejaw"] = "Upper Insatiable Eater Jaw Collection",
- ["usedattributereset"] = "Attribute Reset Used",
- ["usedfreeattributereset"] = "Free Attribute Reset Used",
- ["usedfreeskillreset"] = "Free Skill Reset Used",
- ["usedskillreset"] = "Skill Reset Used",
- ["virindiisland"] = "Singularity Island Visit",
-
- // Kill Tasks
- ["turshscalp"] = "Tursh Scalp",
- ["polarursuin"] = "Polar Ursuin Kill Task Main Flag Timer",
- ["polarursuinkillcount"] = "Polar Ursuin Kill Counter",
- ["polardillotask"] = "Polar Dillo Kill Task Main Flag",
- ["polardillokills"] = "Polar Dillo Kill Counter",
- ["repugnanteaterkilltask"] = "Repugnant Eater Kill Task",
- ["repugeaterkillcount"] = "Repugnant Eater Kill Counter",
- ["deathcap"] = "Deathcap Thrungus Kill Task",
- ["deathcapkillcount"] = "Deathcap Thrungus Kill Counter",
- ["grievverv"] = "Grievver Violator Kill Task",
- ["grievvervkillcount"] = "Grievver Violator Kill Counter",
- ["tuskerg"] = "Tusker Guard Kill Task Main Flag",
- ["tuskergkillcount"] = "Tusker Guard Kill Counter",
-
- // Quest Timers and Pickups
- ["blankaug"] = "Blank Aug Gem Pickup Timer",
- ["greatcavepenguinegg"] = "Great Cave Penguin Egg Pickup Timer",
- ["deathallurecd"] = "Death's Allure Timer Flag",
- ["brewmastercover"] = "Brew Master Quest Pickup Timer Cover",
- ["brewmasterback"] = "Brew Master Quest Pickup Timer Back",
- ["brewmasterpages"] = "Brew Master Quest Pickup Timer Pages",
- ["brewmasterspine"] = "Brew Master Quest Pickup Timer Spine",
- ["eleonorasheart"] = "Elanora's Heart Quest Pickup Timer",
- ["beacongemobtained"] = "Cooldown for obtaining another beacon gem",
- ["beaconcomplete"] = "Beacon Quest Complete Timer",
- ["sirginaziosword"] = "Pick up of Sir Ginazio's Sword",
-
- // Major Quests
- ["maraudersjaw"] = "Marauder's Lair Quest",
- ["fledgemastertusk"] = "Fledge Master's Tusk Quest",
- ["crystallinekiller"] = "Crystalline Killer",
- ["darkisledelivery"] = "Dark Isle Delivery",
- ["defeatingvaeshok"] = "Defeating Vaeshok",
- ["hollyjollyhelperquest"] = "Holly Jolly Helper Quest",
- ["moarsmenjailbreak"] = "Moarsmen Jailbreak",
- ["shamblingarchivistdestroyer"] = "Shambling Archivist Destroyer",
- ["tracingthestone"] = "Tracing The Stone",
- ["undeadjawcollection"] = "Undead Jaw Collection",
- ["weedingofthederutree"] = "Weeding of the Deru Tree",
- ["ironbladecommander"] = "Iron Blade Commander",
- ["mumiyahhuntingneftet"] = "Mumiyah Hunting Neftet",
- ["torgashstasks"] = "Torgash's Tasks",
-
- // Thrungus Hovels Items
- ["stolenfryingpan"] = "Thrungus Hovels",
- ["stolenring"] = "Thrungus Hovels",
- ["stolenbrewkettle"] = "Thrungus Hovels",
- ["stolenamulet"] = "Thrungus Hovels",
- ["stolenewer"] = "Thrungus Hovels",
- ["stolennecklace"] = "Thrungus Hovels",
- ["stolenplatter"] = "Thrungus Hovels",
- ["stolenbracelet"] = "Thrungus Hovels",
-
- // Special Items and Flags
- ["ringofkarlun"] = "Knights of Karlun",
- ["trainingacademycomplete"] = "Completion of Training Academy for Exit",
- ["cowtipcounter"] = "Counter for Cow Tipping",
- ["cowtip"] = "Main Timed Flag for Cow Tipping",
- ["skillloweringgempickedup"] = "Picked up a forgetfulness gem",
-
- // Healing Machine Components
- ["orbhealingmachine"] = "Healing Machine Orb",
- ["pedestalhealingmachine"] = "Healing Machine Pedestal",
- ["tihnhealingmachine"] = "Healing Machine Tihn",
- ["lavushealingmachine"] = "Healing Machine Lavus",
- ["hookhealingmachine"] = "Healing Machine Hook",
-
- // Eater Jaws
- ["ravenouseaterjaw"] = "Ravenous Eater Jaw",
- ["insatiableeaterjaw"] = "Insatiable Eater Jaw",
- ["engorgedeaterjaw"] = "Engorged Eater Jaw",
- ["voraciouseaterjaw"] = "Voracious Eater Jaw",
- ["abhorrenteaterjaw"] = "Abhorrent Eater Jaw",
-
- // Kill Tasks (Extended)
- ["altereddrudgekilltask"] = "Altered Drudge Kill Task",
- ["altereddrudgekillcount"] = "Altered Drudge Kill Counter",
- ["arcticmattekarkilltask"] = "Arctic Mattekar Kill Task",
- ["arcticmattekarkillcount"] = "Arctic Mattekar Kill Counter",
- ["armoredillohuntingneftetkilltask"] = "Armoredillo Hunting Neftet Kill Task",
- ["armoredillohuntingneftetkillcount"] = "Armoredillo Hunting Neftet Kill Counter",
- ["augmenteddrudgekilltask"] = "Augmented Drudge Kill Task",
- ["augmenteddrudgekillcount"] = "Augmented Drudge Kill Counter",
- ["banishedcreaturekilltask"] = "Banished Creature Kill Task",
- ["banishedcreaturekillcount"] = "Banished Creature Kill Counter",
- ["benekniffiskilltask"] = "Benek Niffis Kill Task",
- ["benekniffiskillcount"] = "Benek Niffis Kill Counter",
- ["blackcoralgolemkilltask"] = "Black Coral Golem Kill Task",
- ["blackcoralgolemkillcount"] = "Black Coral Golem Kill Counter",
- ["blessedmoarsmankilltask"] = "Blessed Moarsman Kill Task",
- ["blessedmoarsmankillcount"] = "Blessed Moarsman Kill Counter",
- ["blightedcoralgolemkilltask"] = "Blighted Coral Golem Kill Task",
- ["blightedcoralgolemkillcount"] = "Blighted Coral Golem Kill Counter",
- ["bloodshrethkilltask"] = "Blood Shreth Kill Task",
- ["bloodshrethkillcount"] = "Blood Shreth Kill Counter",
- ["bronzegauntlettrooperkilltask"] = "Bronze Gauntlet Trooper Kill Task",
- ["bronzegauntlettrooperkillcount"] = "Bronze Gauntlet Trooper Kill Counter",
- ["coppercogtrooperkilltask"] = "Copper Cog Trooper Kill Task",
- ["coppercogtrooperkillcount"] = "Copper Cog Trooper Kill Counter",
- ["coppergolemkingpinkilltask"] = "Copper Golem Kingpin Kill Task",
- ["coppergolemkingpinkillcount"] = "Copper Golem Kingpin Kill Counter",
- ["coralgolemkilltask"] = "Coral Golem Kill Task",
- ["coralgolemkillcount"] = "Coral Golem Kill Counter",
- ["coralgolemviceroykilltask"] = "Coral Golem Viceroy Kill Task",
- ["coralgolemviceroykillcount"] = "Coral Golem Viceroy Kill Counter",
- ["corruptedgravestonekilltask"] = "Corrupted Gravestone Kill Task",
- ["corruptedgravestonekillcount"] = "Corrupted Gravestone Kill Counter",
- ["deathcapthrunguskilltask"] = "Deathcap Thrungus Kill Task",
- ["deathcapthrunguskillcount"] = "Deathcap Thrungus Kill Counter",
- ["desertcactuskilltask"] = "Desert Cactus Kill Task",
- ["desertcactuskillcount"] = "Desert Cactus Kill Counter",
- ["devourermargulkilltask"] = "Devourer Margul Kill Task",
- ["devourermargulkillcount"] = "Devourer Margul Kill Counter",
-
- // Society and Faction Quests
- ["celestialhandintroductioncomplete"] = "Celestial Hand Introduction Complete",
- ["eldrytchwebintroductioncomplete"] = "Eldrytch Web Introduction Complete",
- ["radiantbloodintroductioncomplete"] = "Radiant Blood Introduction Complete",
- ["celestialhandinitiatetest"] = "Celestial Hand Initiate Test",
- ["eldrytchwebinitiatetest"] = "Eldrytch Web Initiate Test",
- ["radiantbloodinitiatetest"] = "Radiant Blood Initiate Test",
-
- // Luminance Aura Related
- ["aetheriaredemption"] = "Aetheria Redemption",
- ["aegisofmerc"] = "Aegis of Merc",
- ["lumaugtradein"] = "Luminance Augmentation Trade In",
-
- // Common AC Quests
- ["holtburgtraderskill"] = "Holtburg Trader Skill Quest",
- ["shoushitraderskill"] = "Shoushi Trader Skill Quest",
- ["yaraqtraderskill"] = "Yaraq Trader Skill Quest",
- ["newbiequests"] = "Newbie Academy Quests",
- ["moarsmanraid"] = "Moarsman Raid",
- ["virindiparadox"] = "Virindi Paradox",
- ["portalspace"] = "Portal Space Exploration"
- };
-
- ///
- /// Get friendly name for a quest stamp, with fallback to original stamp
- ///
- /// The quest stamp to lookup
- /// Friendly name if found, otherwise the original quest stamp
- public static string GetFriendlyName(string questStamp)
- {
- if (string.IsNullOrEmpty(questStamp))
- return questStamp;
-
- return QuestStampToName.TryGetValue(questStamp.ToLower(), out string friendlyName)
- ? friendlyName
- : questStamp;
- }
-
- ///
- /// Get display name with friendly name and original stamp in parentheses
- ///
- /// The quest stamp to format
- /// Formatted display name
- public static string GetDisplayName(string questStamp)
- {
- if (string.IsNullOrEmpty(questStamp))
- return questStamp;
-
- string friendlyName = GetFriendlyName(questStamp);
-
- // If we found a mapping, show friendly name with original in parentheses
- if (!string.Equals(friendlyName, questStamp, System.StringComparison.OrdinalIgnoreCase))
- {
- return $"{friendlyName} ({questStamp})";
- }
-
- // Otherwise just show the original
- return questStamp;
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/QuestStreamingService.cs b/MosswartMassacre/QuestStreamingService.cs
deleted file mode 100644
index 5409d35..0000000
--- a/MosswartMassacre/QuestStreamingService.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-using System;
-using System.Linq;
-using System.Timers;
-
-namespace MosswartMassacre
-{
- ///
- /// Streams high-priority quest timer data via WebSocket on a 30-second interval.
- ///
- internal class QuestStreamingService
- {
- private readonly IPluginLogger _logger;
- private Timer _timer;
-
- internal QuestStreamingService(IPluginLogger logger)
- {
- _logger = logger;
- }
-
- internal void Start()
- {
- _timer = new Timer(Constants.QuestStreamingIntervalMs);
- _timer.Elapsed += OnTimerElapsed;
- _timer.AutoReset = true;
- _timer.Start();
- }
-
- internal void Stop()
- {
- if (_timer != null)
- {
- _timer.Stop();
- _timer.Elapsed -= OnTimerElapsed;
- _timer.Dispose();
- _timer = null;
- }
- }
-
- internal bool IsRunning => _timer != null && _timer.Enabled;
-
- private void OnTimerElapsed(object sender, ElapsedEventArgs e)
- {
- try
- {
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- _logger?.Log("[QUEST-STREAM] Timer fired, checking conditions...");
- }
-
- if (!PluginCore.WebSocketEnabled)
- {
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- _logger?.Log("[QUEST-STREAM] WebSocket not enabled, skipping");
- }
- return;
- }
-
- var questManager = PluginCore.questManager;
- if (questManager?.QuestList == null || questManager.QuestList.Count == 0)
- {
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- _logger?.Log($"[QUEST-STREAM] No quest data available (null: {questManager?.QuestList == null}, count: {questManager?.QuestList?.Count ?? 0})");
- }
- return;
- }
-
- var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
-
- var priorityQuests = questManager.QuestList
- .Where(q => IsHighPriorityQuest(q.Id))
- .GroupBy(q => q.Id)
- .Select(g => g.First())
- .ToList();
-
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- _logger?.Log($"[QUEST-STREAM] Found {priorityQuests.Count} priority quests to stream");
- }
-
- foreach (var quest in priorityQuests)
- {
- try
- {
- string questName = questManager.GetFriendlyQuestName(quest.Id);
- long timeRemaining = quest.ExpireTime - currentTime;
- string countdown = FormatCountdown(timeRemaining);
-
- if (PluginSettings.Instance?.VerboseLogging == true)
- {
- _logger?.Log($"[QUEST-STREAM] Sending: {questName} - {countdown}");
- }
-
- System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
- }
- catch (Exception ex)
- {
- _logger?.Log($"[QUEST-STREAM] Error streaming quest {quest.Id}: {ex.Message}");
- }
- }
- }
- catch (Exception ex)
- {
- _logger?.Log($"[QUEST-STREAM] Error in timer handler: {ex.Message}");
- }
- }
-
- internal static bool IsHighPriorityQuest(string questId)
- {
- return questId == "stipendtimer_0812" ||
- questId == "augmentationblankgemacquired" ||
- questId == "insatiableeaterjaw";
- }
-
- internal static string FormatCountdown(long seconds)
- {
- if (seconds <= 0)
- return "READY";
-
- var timeSpan = TimeSpan.FromSeconds(seconds);
-
- if (timeSpan.TotalDays >= 1)
- return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
- else if (timeSpan.TotalHours >= 1)
- return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
- else if (timeSpan.TotalMinutes >= 1)
- return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
- else
- return $"{timeSpan.Seconds}s";
- }
- }
-}
diff --git a/MosswartMassacre/RareTracker.cs b/MosswartMassacre/RareTracker.cs
deleted file mode 100644
index 5bd7c9d..0000000
--- a/MosswartMassacre/RareTracker.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-using Decal.Adapter;
-
-namespace MosswartMassacre
-{
- ///
- /// Tracks rare item discoveries, handles rare meta state toggles,
- /// and sends rare notifications via WebSocket.
- ///
- internal class RareTracker
- {
- private readonly IPluginLogger _logger;
- private readonly string _characterName;
-
- internal int RareCount { get; set; }
- internal bool RareMetaEnabled { get; set; } = true;
-
- internal RareTracker(IPluginLogger logger)
- {
- _logger = logger;
- _characterName = CoreManager.Current.CharacterFilter.Name;
- }
-
- ///
- /// Check if the chat text is a rare discovery by this character.
- /// If so, increments count, triggers meta switch, allegiance announce, and WebSocket notification.
- /// Returns true if a rare was found.
- ///
- internal bool CheckForRare(string text, out string rareText)
- {
- if (IsRareDiscoveryMessage(text, out rareText))
- {
- RareCount++;
-
- if (RareMetaEnabled)
- {
- PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
- }
-
- DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
- _ = WebSocket.SendRareAsync(rareText);
- return true;
- }
- return false;
- }
-
- internal void ToggleRareMeta()
- {
- PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
- }
-
- private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
- {
- rareTextOnly = null;
-
- string pattern = @"^(?['A-Za-z ]+)\shas discovered the (?- .*?)!$";
- Match match = Regex.Match(text, pattern);
-
- if (match.Success && match.Groups["name"].Value == _characterName)
- {
- rareTextOnly = match.Groups["item"].Value;
- return true;
- }
-
- return false;
- }
- }
-}
diff --git a/MosswartMassacre/SpellManager.cs b/MosswartMassacre/SpellManager.cs
deleted file mode 100644
index 5d3732b..0000000
--- a/MosswartMassacre/SpellManager.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using Mag.Shared.Spells;
-
-namespace MosswartMassacre
-{
- ///
- /// Manages spell identification and cantrip detection for the Flag Tracker
- ///
- public static class SpellManager
- {
- private static readonly Dictionary SpellsById = new Dictionary();
- private static readonly List SpellData = new List();
- private static bool isInitialized = false;
-
- static SpellManager()
- {
- Initialize();
- }
-
- private static void Initialize()
- {
- if (isInitialized) return;
-
- try
- {
- // Load spell data from embedded CSV resource
- var assembly = Assembly.GetExecutingAssembly();
-
- // Try to find the resource with different naming patterns
- var availableResources = assembly.GetManifestResourceNames();
- var spellResource = availableResources.FirstOrDefault(r => r.Contains("Spells.csv"));
-
- if (string.IsNullOrEmpty(spellResource))
- {
- // If not embedded, try to load from file system
- var csvPath = Path.Combine(Path.GetDirectoryName(assembly.Location), "..", "Shared", "Spells", "Spells.csv");
- if (File.Exists(csvPath))
- {
- LoadFromFile(csvPath);
- isInitialized = true;
- return;
- }
- }
- else
- {
- using (var stream = assembly.GetManifestResourceStream(spellResource))
- {
- if (stream != null)
- {
- using (var reader = new StreamReader(stream))
- {
- LoadFromReader(reader);
- }
- }
- }
- }
-
- isInitialized = true;
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"SpellManager initialization error: {ex.Message}");
- }
- }
-
- private static void LoadFromFile(string path)
- {
- using (var reader = new StreamReader(path))
- {
- LoadFromReader(reader);
- }
- }
-
- private static void LoadFromReader(StreamReader reader)
- {
- // Skip header line
- var header = reader.ReadLine();
-
- while (!reader.EndOfStream)
- {
- var line = reader.ReadLine();
- if (!string.IsNullOrWhiteSpace(line))
- {
- var parts = line.Split(',');
- if (parts.Length >= 6) // Minimum required fields
- {
- SpellData.Add(parts);
-
- // Parse spell data
- if (int.TryParse(parts[0], out int id))
- {
- var name = parts[1];
- int.TryParse(parts[3], out int difficulty);
- int.TryParse(parts[4], out int duration);
- int.TryParse(parts[5], out int family);
-
- var spell = new Spell(id, name, difficulty, duration, family);
- SpellsById[id] = spell;
- }
- }
- }
- }
- }
-
- ///
- /// Gets a spell by its ID
- ///
- public static Spell GetSpell(int id)
- {
- if (SpellsById.TryGetValue(id, out var spell))
- return spell;
- return null;
- }
-
- ///
- /// Gets a spell by its name (case-insensitive)
- ///
- public static Spell GetSpell(string name)
- {
- return SpellsById.Values.FirstOrDefault(s =>
- string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
- }
-
- ///
- /// Gets the total number of spells loaded
- ///
- public static int GetSpellCount()
- {
- return SpellsById.Count;
- }
-
- ///
- /// Detects if a spell is a cantrip and returns its info
- ///
- public static CantripInfo DetectCantrip(Spell spell)
- {
- if (spell == null || spell.CantripLevel == Spell.CantripLevels.None)
- return null;
-
- var info = new CantripInfo
- {
- SpellId = spell.Id,
- Name = spell.Name,
- Level = GetCantripLevelName(spell.CantripLevel),
- Color = GetCantripColor(spell.CantripLevel)
- };
-
- // Extract skill/attribute name from spell name
- info.SkillName = ExtractSkillFromSpellName(spell.Name, info.Level);
-
- return info;
- }
-
- private static string GetCantripLevelName(Spell.CantripLevels level)
- {
- switch (level)
- {
- case Spell.CantripLevels.Minor: return "Minor";
- case Spell.CantripLevels.Moderate: return "Moderate";
- case Spell.CantripLevels.Major: return "Major";
- case Spell.CantripLevels.Epic: return "Epic";
- case Spell.CantripLevels.Legendary: return "Legendary";
- default: return "N/A";
- }
- }
-
- private static System.Drawing.Color GetCantripColor(Spell.CantripLevels level)
- {
- switch (level)
- {
- case Spell.CantripLevels.Minor: return System.Drawing.Color.White;
- case Spell.CantripLevels.Moderate: return System.Drawing.Color.Green;
- case Spell.CantripLevels.Major: return System.Drawing.Color.Blue;
- case Spell.CantripLevels.Epic: return System.Drawing.Color.Purple;
- case Spell.CantripLevels.Legendary: return System.Drawing.Color.Orange;
- default: return System.Drawing.Color.White;
- }
- }
-
- private static string ExtractSkillFromSpellName(string spellName, string level)
- {
- // Remove the cantrip level prefix
- var skillPart = spellName;
- if (!string.IsNullOrEmpty(level) && skillPart.StartsWith(level + " "))
- {
- skillPart = skillPart.Substring(level.Length + 1);
- }
-
- // Map common spell name patterns to skill names
- if (skillPart.Contains("Strength")) return "Strength";
- if (skillPart.Contains("Endurance")) return "Endurance";
- if (skillPart.Contains("Coordination")) return "Coordination";
- if (skillPart.Contains("Quickness")) return "Quickness";
- if (skillPart.Contains("Focus")) return "Focus";
- if (skillPart.Contains("Self") || skillPart.Contains("Willpower")) return "Willpower";
-
- // Protection mappings
- if (skillPart.Contains("Armor")) return "Armor";
- if (skillPart.Contains("Bludgeoning")) return "Bludgeoning Ward";
- if (skillPart.Contains("Piercing")) return "Piercing Ward";
- if (skillPart.Contains("Slashing")) return "Slashing Ward";
- if (skillPart.Contains("Flame") || skillPart.Contains("Fire")) return "Flame Ward";
- if (skillPart.Contains("Frost") || skillPart.Contains("Cold")) return "Frost Ward";
- if (skillPart.Contains("Acid")) return "Acid Ward";
- if (skillPart.Contains("Lightning") || skillPart.Contains("Electric")) return "Storm Ward";
-
- // Return the skill part as-is if no mapping found
- return skillPart;
- }
-
- ///
- /// Information about a detected cantrip
- ///
- public class CantripInfo
- {
- public int SpellId { get; set; }
- public string Name { get; set; }
- public string SkillName { get; set; }
- public string Level { get; set; }
- public System.Drawing.Color Color { get; set; }
- }
- }
-}
\ No newline at end of file
diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs
new file mode 100644
index 0000000..ce9bb79
--- /dev/null
+++ b/MosswartMassacre/Telemetry.cs
@@ -0,0 +1,109 @@
+// Telemetry.cs ───────────────────────────────────────────────────────────────
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Decal.Adapter;
+using Newtonsoft.Json;
+
+namespace MosswartMassacre
+{
+ public static class Telemetry
+ {
+ /* ───────────── configuration ───────────── */
+ private const string Endpoint = "https://mosswart.snakedesert.se/position/"; // <- trailing slash!
+ private const string SharedSecret = "your_shared_secret"; // <- keep in sync
+ private const int IntervalSec = 5; // seconds between posts
+
+ /* ───────────── runtime state ───────────── */
+ private static readonly HttpClient _http = new HttpClient();
+ private static string _sessionId;
+ private static CancellationTokenSource _cts;
+ private static bool _enabled;
+
+ /* ───────────── public API ───────────── */
+ public static void Start()
+ {
+ if (_enabled) return;
+
+ _enabled = true;
+ _sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
+ _cts = new CancellationTokenSource();
+
+ PluginCore.WriteToChat("[Telemetry] HTTP streaming ENABLED");
+
+ _ = Task.Run(() => LoopAsync(_cts.Token)); // fire-and-forget
+ }
+
+ public static void Stop()
+ {
+ if (!_enabled) return;
+ _cts.Cancel();
+ _enabled = false;
+ PluginCore.WriteToChat("[Telemetry] HTTP streaming DISABLED");
+ }
+
+ /* ───────────── async loop ───────────── */
+ private static async Task LoopAsync(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ try
+ {
+ await SendSnapshotAsync(token);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[Telemetry] send failed: {ex.Message}");
+ }
+
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(IntervalSec), token);
+ }
+ catch (TaskCanceledException) { } // expected on Stop()
+ }
+ }
+
+ /* ───────────── single POST ───────────── */
+ private static async Task SendSnapshotAsync(CancellationToken token)
+ {
+ var coords = Coordinates.Me;
+
+ var payload = new
+ {
+ character_name = CoreManager.Current.CharacterFilter.Name,
+ char_tag = PluginCore.CharTag,
+ session_id = _sessionId,
+ timestamp = DateTime.UtcNow.ToString("o"),
+
+ ew = coords.EW,
+ ns = coords.NS,
+ z = coords.Z,
+
+ kills = PluginCore.totalKills,
+ onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
+ kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
+ deaths = 0,
+ rares_found = PluginCore.rareCount,
+ prismatic_taper_count = 0,
+ vt_state = VtankControl.VtGetMetaState(),
+ };
+
+ string json = JsonConvert.SerializeObject(payload);
+ var req = new HttpRequestMessage(HttpMethod.Post, Endpoint)
+ {
+ Content = new StringContent(json, Encoding.UTF8, "application/json")
+ };
+ req.Headers.Add("X-Plugin-Secret", SharedSecret);
+
+ using var resp = await _http.SendAsync(req, token);
+
+ if (!resp.IsSuccessStatusCode) // stay quiet on success
+ {
+ PluginCore.WriteToChat($"[Telemetry] server replied {resp.StatusCode}");
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/UpdateManager.cs b/MosswartMassacre/UpdateManager.cs
deleted file mode 100644
index 6eaa70c..0000000
--- a/MosswartMassacre/UpdateManager.cs
+++ /dev/null
@@ -1,253 +0,0 @@
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Security.Cryptography;
-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 string remoteFileHash = string.Empty;
- private static string localFileHash = string.Empty;
- private static DateTime lastCheckTime = DateTime.MinValue;
-
- public static bool IsUpdateAvailable => updateAvailable;
- public static DateTime LastCheckTime => lastCheckTime;
-
- ///
- /// Calculate SHA256 hash of a file
- ///
- private static string CalculateFileHash(string filePath)
- {
- using (var sha256 = SHA256.Create())
- {
- using (var stream = File.OpenRead(filePath))
- {
- byte[] hashBytes = sha256.ComputeHash(stream);
- return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
- }
- }
- }
-
- ///
- /// Calculate SHA256 hash of byte array
- ///
- private static string CalculateHash(byte[] data)
- {
- using (var sha256 = SHA256.Create())
- {
- byte[] hashBytes = sha256.ComputeHash(data);
- return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
- }
- }
-
- public static async Task CheckForUpdateAsync()
- {
- try
- {
- PluginCore.WriteToChat("[Update] Checking for updates...");
-
- // Get local file hash
- string localPath = GetLocalDllPath();
- if (!File.Exists(localPath))
- {
- PluginCore.WriteToChat("[Update] Error: Could not find local DLL file");
- return false;
- }
-
- PluginCore.WriteToChat("[Update] Calculating local file hash...");
- localFileHash = CalculateFileHash(localPath);
-
- // Download remote file and calculate hash
- using (var client = new HttpClient())
- {
- client.Timeout = TimeSpan.FromSeconds(30);
-
- PluginCore.WriteToChat("[Update] Downloading remote file for comparison...");
- var remoteData = await client.GetByteArrayAsync(UPDATE_URL);
-
- if (remoteData == null || remoteData.Length == 0)
- {
- PluginCore.WriteToChat("[Update] Error: Could not download remote file");
- return false;
- }
-
- PluginCore.WriteToChat("[Update] Calculating remote file hash...");
- remoteFileHash = CalculateHash(remoteData);
- }
-
- // Compare hashes
- updateAvailable = !string.Equals(localFileHash, remoteFileHash, StringComparison.OrdinalIgnoreCase);
- lastCheckTime = DateTime.Now;
-
- if (updateAvailable)
- {
- PluginCore.WriteToChat($"[Update] Update available!");
- PluginCore.WriteToChat($"[Update] Local hash: {localFileHash}");
- PluginCore.WriteToChat($"[Update] Remote hash: {remoteFileHash}");
- }
- else
- {
- PluginCore.WriteToChat("[Update] Up to date - hashes match");
- }
-
- 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 by hash
- PluginCore.WriteToChat("[Update] Validating downloaded file...");
- var downloadedHash = CalculateFileHash(tempPath);
- if (!string.Equals(downloadedHash, remoteFileHash, StringComparison.OrdinalIgnoreCase))
- {
- File.Delete(tempPath);
- PluginCore.WriteToChat($"[Update] Download validation failed. Hash mismatch!");
- PluginCore.WriteToChat($"[Update] Expected: {remoteFileHash}");
- PluginCore.WriteToChat($"[Update] Got: {downloadedHash}");
- 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;
- }
- }
-
- ///
- /// Check for update and auto-install if available. Used by startup auto-update.
- ///
- public static async Task CheckAndInstallAsync()
- {
- try
- {
- bool checkOk = await CheckForUpdateAsync();
- if (checkOk && updateAvailable)
- {
- PluginCore.WriteToChat("[Update] Auto-installing update...");
- await DownloadAndInstallUpdateAsync();
- }
- }
- catch (Exception ex)
- {
- PluginCore.WriteToChat($"[Update] Auto-update failed: {ex.Message}");
- }
- }
-
- 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/Utils.cs b/MosswartMassacre/Utils.cs
index c6a7381..5a63a12 100644
--- a/MosswartMassacre/Utils.cs
+++ b/MosswartMassacre/Utils.cs
@@ -2,8 +2,6 @@
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using System.Numerics;
-using Mag.Shared.Constants;
-using System.Linq;
namespace MosswartMassacre
{
@@ -41,29 +39,6 @@ namespace MosswartMassacre
}
}
- ///
- /// Return any WorldObject's raw world position by reading the
- /// physics-object pointer (same offsets: +0x84/88/8C).
- ///
- public static unsafe Vector3 GetWorldObjectPosition(int objectId)
- {
- try
- {
- if (!CoreManager.Current.Actions.IsValidObject(objectId))
- return new Vector3();
-
- byte* p = (byte*)CoreManager.Current.Actions.Underlying.GetPhysicsObjectPtr(objectId);
- return new Vector3(
- *(float*)(p + 0x84), // X
- *(float*)(p + 0x88), // Y
- *(float*)(p + 0x8C)); // Z
- }
- catch
- {
- return new Vector3();
- }
- }
-
///
/// Convenience: returns the current landcell (upper 16 bits of landblock).
///
@@ -88,26 +63,6 @@ namespace MosswartMassacre
return new Coordinates(ew, ns, pos.Z);
}
- ///
- /// Get AC-style coordinates (EW/NS/Z) for any WorldObject.
- ///
- public static Coordinates GetWorldObjectCoordinates(WorldObject wo)
- {
- if (wo == null) return new Coordinates();
-
- Vector3 pos = GetWorldObjectPosition(wo.Id);
-
- // Get landcell from the object's coordinates
- var coordsObj = wo.Coordinates();
- if (coordsObj == null) return new Coordinates();
-
- // Convert DECAL coords to our Coordinates with Z
- double ew = coordsObj.EastWest;
- double ns = coordsObj.NorthSouth;
-
- return new Coordinates(ew, ns, pos.Z);
- }
-
/* ----------------------------------------------------------
* 3) Generic math helpers you may want later
* -------------------------------------------------------- */
@@ -117,212 +72,5 @@ namespace MosswartMassacre
public static double DegToRad(double deg) => deg * Math.PI / 180.0;
public static double RadToDeg(double rad) => rad * 180.0 / Math.PI;
-
- /* ----------------------------------------------------------
- * 4) Generic item property access
- * -------------------------------------------------------- */
-
- ///
- /// Find a WorldObject item by name in inventory
- ///
- /// Name of the item to find
- /// WorldObject or null if not found
- public static WorldObject FindItemByName(string itemName)
- {
- try
- {
- //var worldFilter = CoreManager.Current.WorldFilter;
- //var playerInv = CoreManager.Current.CharacterFilter.Id;
-
- // Search inventory
-
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- if (string.Equals(wo.Name, itemName, StringComparison.OrdinalIgnoreCase))
- return wo;
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// Get the stack size/quantity of a specific item by name
- ///
- /// Name of the item to find
- /// Stack size or 0 if not found
- ///
- /// Return the total quantity of an item in the character’s inventory,
- /// adding up every stack that shares .
- ///
- public static int GetItemStackSize(string itemName)
- {
- try
- {
- // 1. Pull every WorldObject in bags + containers
- var inv = CoreManager.Current.WorldFilter.GetInventory();
-
- // 2. Keep only those whose display name matches (case-insensitive)
- // 3. For each one, use StackCount if it exists, otherwise treat as 1
- return inv.Where(wo =>
- string.Equals(wo.Name, itemName,
- StringComparison.OrdinalIgnoreCase))
- .Sum(wo =>
- {
- // Some items (weapons, armor) aren’t stackable;
- // Values(LongValueKey.StackCount) throws if the key is absent.
- try
- {
- return wo.Values(LongValueKey.StackCount);
- }
- catch
- {
- return 1; // non-stackable item = quantity 1
- }
- });
- }
- catch
- {
- return 0;
- }
- }
-
-
- ///
- /// Get the icon ID of a specific item by name
- ///
- /// Name of the item to find
- /// Icon ID or 0 if not found
- public static int GetItemIcon(string itemName)
- {
- try
- {
- var item = FindItemByName(itemName);
- return item?.Icon ?? 0;
- }
- catch
- {
- return 0;
- }
- }
-
- ///
- /// Get the display icon ID (with 0x6000000 offset) for an item by name
- ///
- /// Name of the item to find
- /// Display icon ID or 0x6002D14 (default icon) if not found
- public static int GetItemDisplayIcon(string itemName)
- {
- int rawIcon = GetItemIcon(itemName);
- return rawIcon != 0 ? rawIcon + 0x6000000 : 0x6002D14;
- }
-
- /* ----------------------------------------------------------
- * 5) Chest Looter helper methods
- * -------------------------------------------------------- */
-
- ///
- /// Calculate 3D distance from player to a world object
- ///
- /// World object ID
- /// Distance in meters, or float.MaxValue if object is invalid
- public static float GetDistanceToWorldObject(int objectId)
- {
- try
- {
- if (!CoreManager.Current.Actions.IsValidObject(objectId))
- return float.MaxValue;
-
- Vector3 playerPos = GetPlayerPosition();
- Vector3 objectPos = GetWorldObjectPosition(objectId);
-
- return Vector3.Distance(playerPos, objectPos);
- }
- catch
- {
- return float.MaxValue;
- }
- }
-
- ///
- /// Find the closest chest with the specified name in the game world
- ///
- /// Name of the chest to find
- /// WorldObject of the closest chest, or null if not found
- public static WorldObject FindClosestChestByName(string chestName)
- {
- try
- {
- WorldObject closestChest = null;
- float closestDistance = float.MaxValue;
-
- // Search all objects in WorldFilter
- using (var objects = CoreManager.Current.WorldFilter.GetAll())
- {
- foreach (WorldObject wo in objects)
- {
- // Check if this is a container (chest)
- if (wo.ObjectClass != ObjectClass.Container)
- continue;
-
- // Check if name matches (case-insensitive, partial match allowed)
- if (!wo.Name.Contains(chestName) &&
- !string.Equals(wo.Name, chestName, StringComparison.OrdinalIgnoreCase))
- continue;
-
- // Calculate distance
- float distance = GetDistanceToWorldObject(wo.Id);
-
- // Update closest if this is nearer
- if (distance < closestDistance)
- {
- closestDistance = distance;
- closestChest = wo;
- }
- }
- }
-
- return closestChest;
- }
- catch
- {
- return null;
- }
- }
-
- ///
- /// Find a key in the player's inventory by name
- ///
- /// Name of the key to find
- /// WorldObject of the key, or null if not found
- public static WorldObject FindKeyInInventory(string keyName)
- {
- try
- {
- foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
- {
- // Check if this is a key
- if (wo.ObjectClass != ObjectClass.Key)
- continue;
-
- // Check if name matches (case-insensitive, partial match allowed)
- if (wo.Name.Contains(keyName) ||
- string.Equals(wo.Name, keyName, StringComparison.OrdinalIgnoreCase))
- {
- return wo;
- }
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
}
}
diff --git a/MosswartMassacre/ViewSystemSelector.cs b/MosswartMassacre/ViewSystemSelector.cs
new file mode 100644
index 0000000..5e75b7a
--- /dev/null
+++ b/MosswartMassacre/ViewSystemSelector.cs
@@ -0,0 +1,262 @@
+///////////////////////////////////////////////////////////////////////////////
+//File: ViewSystemSelector.cs
+//
+//Description: Contains the MyClasses.MetaViewWrappers.ViewSystemSelector class,
+// which is used to determine whether the Virindi View Service is enabled.
+// As with all the VVS wrappers, the VVS_REFERENCED compilation symbol must be
+// defined for the VVS code to be compiled. Otherwise, only Decal views are used.
+//
+//References required:
+// VirindiViewService (if VVS_REFERENCED is defined)
+// Decal.Adapter
+// Decal.Interop.Core
+//
+//This file is Copyright (c) 2009 VirindiPlugins
+//
+//Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+//The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+///////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Reflection;
+
+#if METAVIEW_PUBLIC_NS
+namespace MetaViewWrappers
+#else
+namespace MyClasses.MetaViewWrappers
+#endif
+{
+ internal static class ViewSystemSelector
+ {
+ public enum eViewSystem
+ {
+ DecalInject,
+ VirindiViewService,
+ }
+
+
+ ///////////////////////////////System presence detection///////////////////////////////
+
+ public static bool IsPresent(Decal.Adapter.Wrappers.PluginHost pHost, eViewSystem VSystem)
+ {
+ switch (VSystem)
+ {
+ case eViewSystem.DecalInject:
+ return true;
+ case eViewSystem.VirindiViewService:
+ return VirindiViewsPresent(pHost);
+ default:
+ return false;
+ }
+ }
+ static bool VirindiViewsPresent(Decal.Adapter.Wrappers.PluginHost pHost)
+ {
+#if VVS_REFERENCED
+ System.Reflection.Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
+
+ foreach (System.Reflection.Assembly a in asms)
+ {
+ AssemblyName nmm = a.GetName();
+ if ((nmm.Name == "VirindiViewService") && (nmm.Version >= new System.Version("1.0.0.37")))
+ {
+ try
+ {
+ return Curtain_VVS_Running();
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ return false;
+#else
+ return false;
+#endif
+ }
+ public static bool VirindiViewsPresent(Decal.Adapter.Wrappers.PluginHost pHost, Version minver)
+ {
+#if VVS_REFERENCED
+ System.Reflection.Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
+
+ foreach (System.Reflection.Assembly a in asms)
+ {
+ AssemblyName nm = a.GetName();
+ if ((nm.Name == "VirindiViewService") && (nm.Version >= minver))
+ {
+ try
+ {
+ return Curtain_VVS_Running();
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ return false;
+#else
+ return false;
+#endif
+ }
+
+#if VVS_REFERENCED
+ static bool Curtain_VVS_Running()
+ {
+ return VirindiViewService.Service.Running;
+ }
+#endif
+
+ ///////////////////////////////CreateViewResource///////////////////////////////
+
+ public static IView CreateViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
+ {
+#if VVS_REFERENCED
+ if (IsPresent(pHost, eViewSystem.VirindiViewService))
+ return CreateViewResource(pHost, pXMLResource, eViewSystem.VirindiViewService);
+ else
+#endif
+ return CreateViewResource(pHost, pXMLResource, eViewSystem.DecalInject);
+ }
+ public static IView CreateViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource, eViewSystem VSystem)
+ {
+ if (!IsPresent(pHost, VSystem)) return null;
+ switch (VSystem)
+ {
+ case eViewSystem.DecalInject:
+ return CreateDecalViewResource(pHost, pXMLResource);
+ case eViewSystem.VirindiViewService:
+#if VVS_REFERENCED
+ return CreateMyHudViewResource(pHost, pXMLResource);
+#else
+ break;
+#endif
+ }
+ return null;
+ }
+ static IView CreateDecalViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
+ {
+ IView ret = new DecalControls.View();
+ ret.Initialize(pHost, pXMLResource);
+ return ret;
+ }
+
+#if VVS_REFERENCED
+ static IView CreateMyHudViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
+ {
+ IView ret = new VirindiViewServiceHudControls.View();
+ ret.Initialize(pHost, pXMLResource);
+ return ret;
+ }
+#endif
+
+
+ ///////////////////////////////CreateViewXML///////////////////////////////
+
+ public static IView CreateViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
+ {
+#if VVS_REFERENCED
+ if (IsPresent(pHost, eViewSystem.VirindiViewService))
+ return CreateViewXML(pHost, pXML, eViewSystem.VirindiViewService);
+ else
+#endif
+ return CreateViewXML(pHost, pXML, eViewSystem.DecalInject);
+ }
+
+ public static IView CreateViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML, eViewSystem VSystem)
+ {
+ if (!IsPresent(pHost, VSystem)) return null;
+ switch (VSystem)
+ {
+ case eViewSystem.DecalInject:
+ return CreateDecalViewXML(pHost, pXML);
+ case eViewSystem.VirindiViewService:
+#if VVS_REFERENCED
+ return CreateMyHudViewXML(pHost, pXML);
+#else
+ break;
+#endif
+ }
+ return null;
+ }
+ static IView CreateDecalViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
+ {
+ IView ret = new DecalControls.View();
+ ret.InitializeRawXML(pHost, pXML);
+ return ret;
+ }
+
+#if VVS_REFERENCED
+ static IView CreateMyHudViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
+ {
+ IView ret = new VirindiViewServiceHudControls.View();
+ ret.InitializeRawXML(pHost, pXML);
+ return ret;
+ }
+#endif
+
+
+ ///////////////////////////////HasChatOpen///////////////////////////////
+
+ public static bool AnySystemHasChatOpen(Decal.Adapter.Wrappers.PluginHost pHost)
+ {
+ if (IsPresent(pHost, eViewSystem.VirindiViewService))
+ if (HasChatOpen_VirindiViews()) return true;
+ if (pHost.Actions.ChatState) return true;
+ return false;
+ }
+
+ static bool HasChatOpen_VirindiViews()
+ {
+#if VVS_REFERENCED
+ if (VirindiViewService.HudView.FocusControl != null)
+ {
+ if (VirindiViewService.HudView.FocusControl.GetType() == typeof(VirindiViewService.Controls.HudTextBox))
+ return true;
+ }
+ return false;
+#else
+ return false;
+#endif
+ }
+
+ public delegate void delConditionalSplit(object data);
+ public static void ViewConditionalSplit(IView v, delConditionalSplit onDecal, delConditionalSplit onVVS, object data)
+ {
+ Type vtype = v.GetType();
+
+#if VVS_REFERENCED
+ if (vtype == typeof(VirindiViewServiceHudControls.View))
+ {
+ if (onVVS != null)
+ onVVS(data);
+ }
+#endif
+
+ if (vtype == typeof(DecalControls.View))
+ {
+ if (onDecal != null)
+ onDecal(data);
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/ViewXML/flagTracker.xml b/MosswartMassacre/ViewXML/flagTracker.xml
deleted file mode 100644
index 565f9d6..0000000
--- a/MosswartMassacre/ViewXML/flagTracker.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-