diff --git a/MosswartMassacre/InventoryMonitor.cs b/MosswartMassacre/InventoryMonitor.cs
new file mode 100644
index 0000000..1f60313
--- /dev/null
+++ b/MosswartMassacre/InventoryMonitor.cs
@@ -0,0 +1,184 @@
+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/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index e3c0829..6c55cb8 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -307,7 +307,9 @@
+
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 2179753..5dc5331 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -47,13 +47,9 @@ namespace MosswartMassacre
public static bool IsHotReload { get; set; }
internal static PluginHost MyHost;
- internal static int rareCount = 0;
- internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count
- internal static int lastPrismaticCount = 0; // For delta calculation
-
- // Track taper items and their containers for accurate release detection
- private static readonly Dictionary trackedTaperContainers = new Dictionary();
- private static readonly Dictionary lastKnownStackSizes = new Dictionary();
+ // Bridge properties for WebSocket telemetry until IGameStats migration (Phase 5)
+ private static InventoryMonitor _staticInventoryMonitor;
+ internal static int cachedPrismaticCount => _staticInventoryMonitor?.CachedPrismaticCount ?? 0;
// Bridge properties for WebSocket telemetry until IGameStats migration (Phase 5)
private static KillTracker _staticKillTracker;
internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
@@ -66,7 +62,12 @@ namespace MosswartMassacre
private static System.Windows.Forms.Timer commandTimer;
private static Timer characterStatsTimer;
private static readonly Queue pendingCommands = new Queue();
- public static bool RareMetaEnabled { get; set; } = true;
+ 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
@@ -123,12 +124,12 @@ namespace MosswartMassacre
public static QuestManager questManager;
private static Timer questStreamingTimer;
- 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 CommandRouter _commandRouter;
protected override void Startup()
@@ -180,10 +181,12 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
- // Subscribe to inventory change events for taper tracking
- CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate;
- CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease;
- CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange;
+ // Initialize inventory monitor (taper tracking)
+ _inventoryMonitor = new InventoryMonitor(this);
+ _staticInventoryMonitor = _inventoryMonitor;
+ CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
+ CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
+ CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
// Initialize VVS view after character login
ViewManager.ViewInit();
@@ -262,10 +265,13 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject -= OnDespawn;
- // Unsubscribe from inventory change events
- CoreManager.Current.WorldFilter.CreateObject -= OnInventoryCreate;
- CoreManager.Current.WorldFilter.ReleaseObject -= OnInventoryRelease;
- CoreManager.Current.WorldFilter.ChangeObject -= OnInventoryChange;
+ // Unsubscribe inventory monitor
+ if (_inventoryMonitor != 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 -= EchoFilter_ServerDispatch;
@@ -329,8 +335,7 @@ namespace MosswartMassacre
}
// Clean up taper tracking
- trackedTaperContainers.Clear();
- lastKnownStackSizes.Clear();
+ _inventoryMonitor?.Cleanup();
// Clean up Harmony patches
DecalHarmonyClean.Cleanup();
@@ -367,8 +372,12 @@ namespace MosswartMassacre
WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
}
+ // Initialize rare tracker
+ _rareTracker = new RareTracker(this);
+ _staticRareTracker = _rareTracker;
+
// Apply the values
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
CharTag = PluginSettings.Instance.CharTag;
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
@@ -394,7 +403,7 @@ namespace MosswartMassacre
_killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// Initialize cached Prismatic Taper count
- InitializePrismaticTaperCount();
+ _inventoryMonitor.Initialize();
// Initialize quest manager for always-on quest streaming
try
@@ -570,7 +579,7 @@ namespace MosswartMassacre
}
// 2. Apply the values from settings
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ if (_rareTracker != null) _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
CharTag = PluginSettings.Instance.CharTag;
@@ -607,7 +616,7 @@ namespace MosswartMassacre
_killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// 7. Reinitialize cached Prismatic Taper count
- InitializePrismaticTaperCount();
+ _inventoryMonitor?.Initialize();
// 8. Reinitialize quest manager for hot reload
try
@@ -659,173 +668,6 @@ namespace MosswartMassacre
WriteToChat("Hot reload initialization completed!");
}
- private void InitializePrismaticTaperCount()
- {
- try
- {
- lastPrismaticCount = cachedPrismaticCount;
- cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
-
- // Initialize tracking for existing tapers
- 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)
- {
- WriteToChat($"[TAPER] Error initializing count: {ex.Message}");
- cachedPrismaticCount = 0;
- lastPrismaticCount = 0;
- trackedTaperContainers.Clear();
- lastKnownStackSizes.Clear();
- }
- }
-
- private bool IsPlayerOwnedContainer(int containerId)
- {
- try
- {
- // Check if it's the player's main inventory
- if (containerId == CoreManager.Current.CharacterFilter.Id)
- return true;
-
- // Check if it's one of the player's containers (side packs)
- // Get the container object to verify it belongs to the player
- 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;
- }
- }
-
- private 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;
- int delta = cachedPrismaticCount - lastPrismaticCount;
-
- // Initialize tracking for this new taper
- trackedTaperContainers[item.Id] = item.Container;
- lastKnownStackSizes[item.Id] = stackCount;
-
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"[TAPER] Error in OnInventoryCreate: {ex.Message}");
- }
- }
-
- private void OnInventoryRelease(object sender, ReleaseObjectEventArgs e)
- {
- try
- {
- var item = e.Released;
- if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
- {
- // Check where this taper WAS before being released (not where it's going)
- if (trackedTaperContainers.TryGetValue(item.Id, out int previousContainer))
- {
- if (IsPlayerOwnedContainer(previousContainer))
- {
- // This taper was in our inventory and is now being released
- lastPrismaticCount = cachedPrismaticCount;
- int stackCount = item.Values(LongValueKey.StackCount, 1);
- cachedPrismaticCount -= stackCount;
- }
-
- // Clean up tracking
- trackedTaperContainers.Remove(item.Id);
- lastKnownStackSizes.Remove(item.Id);
- }
- else
- {
- // Fallback: recalculate total count when untracked taper is released
- lastPrismaticCount = cachedPrismaticCount;
- cachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
- }
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"[TAPER] Error in OnInventoryRelease: {ex.Message}");
- }
- }
-
- private void OnInventoryChange(object sender, ChangeObjectEventArgs e)
- {
- try
- {
- var item = e.Changed;
- if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
- {
- bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container);
-
- // Track container location for release detection
- if (isInPlayerContainer)
- {
- bool wasAlreadyTracked = trackedTaperContainers.ContainsKey(item.Id);
- trackedTaperContainers[item.Id] = item.Container;
-
- // Handle stack size changes with pure delta math
- int currentStack = item.Values(LongValueKey.StackCount, 1);
-
- // Check if this is a pickup from ground (item not previously tracked)
- if (!wasAlreadyTracked)
- {
- // This is likely a pickup from ground - increment count
- 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;
- }
- // Item is no longer in player containers
- // DON'T clean up tracking here - let OnInventoryRelease handle cleanup
- // This ensures tracking data is available for the Release event
- }
- }
- catch (Exception ex)
- {
- WriteToChat($"[TAPER] Error in OnInventoryChange: {ex.Message}");
- }
- }
private async void OnSpawn(object sender, CreateObjectEventArgs e)
{
@@ -1039,26 +881,16 @@ namespace MosswartMassacre
_killTracker.CheckForKill(e.Text);
- if (IsRareDiscoveryMessage(e.Text, out string rareText))
+ if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
{
- _killTracker.RareCount++;
- rareCount = _killTracker.RareCount; // sync static for now
- ViewManager.UpdateRareCount(_killTracker.RareCount);
-
- if (RareMetaEnabled)
- {
- Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
- }
-
- DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
- // Fire and forget: we don't await, since sending is not critical and we don't want to block.
- _ = WebSocket.SendRareAsync(rareText);
+ _killTracker.RareCount = _rareTracker.RareCount;
+ ViewManager.UpdateRareCount(_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: {_killTracker.RareCount}";
+ string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}";
WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
}
@@ -1187,22 +1019,6 @@ namespace MosswartMassacre
}
}
- private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
- {
- rareTextOnly = null;
-
- // Match pattern: " has discovered the !"
- string pattern = @"^(?['A-Za-z ]+)\shas discovered the (?- .*?)!$";
- Match match = Regex.Match(text, pattern);
-
- if (match.Success && match.Groups["name"].Value == CoreManager.Current.CharacterFilter.Name)
- {
- rareTextOnly = match.Groups["item"].Value; // just "Ancient Pickle"
- return true;
- }
-
- return false;
- }
public static void WriteToChat(string message)
{
try
@@ -1237,12 +1053,13 @@ namespace MosswartMassacre
public static void RestartStats()
{
_staticKillTracker?.RestartStats();
- ViewManager.UpdateRareCount(_staticKillTracker?.RareCount ?? 0);
+ if (_staticRareTracker != null)
+ _staticRareTracker.RareCount = 0;
+ ViewManager.UpdateRareCount(0);
}
public static void ToggleRareMeta()
{
- PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
- RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
+ _staticRareTracker?.ToggleRareMeta();
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}
@@ -1307,7 +1124,7 @@ namespace MosswartMassacre
_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: {_killTracker.RareCount}, Session Deaths: {_killTracker.SessionDeaths}, Total Deaths: {_killTracker.TotalDeaths}";
+ 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 current stats");
@@ -1617,21 +1434,21 @@ namespace MosswartMassacre
try
{
WriteToChat("=== Cached Taper Tracking Test ===");
- WriteToChat($"Cached Count: {cachedPrismaticCount}");
- WriteToChat($"Last Count: {lastPrismaticCount}");
+ WriteToChat($"Cached Count: {_inventoryMonitor.CachedPrismaticCount}");
+ WriteToChat($"Last Count: {_inventoryMonitor.LastPrismaticCount}");
int utilsCount = Utils.GetItemStackSize("Prismatic Taper");
WriteToChat($"Utils Count: {utilsCount}");
- if (cachedPrismaticCount == utilsCount)
+ if (_inventoryMonitor.CachedPrismaticCount == utilsCount)
{
WriteToChat("[OK] Cached count matches Utils count");
}
else
{
- WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Utils: {utilsCount}");
+ WriteToChat($"[WARNING] Count mismatch! Cached: {_inventoryMonitor.CachedPrismaticCount}, Utils: {utilsCount}");
WriteToChat("Refreshing cached count...");
- InitializePrismaticTaperCount();
+ _inventoryMonitor.Initialize();
}
WriteToChat("=== Container Analysis ===");
@@ -1656,8 +1473,8 @@ namespace MosswartMassacre
WriteToChat($"Total: {mainPackCount + sidePackCount}");
WriteToChat("=== Event System Status ===");
- WriteToChat($"Tracking {trackedTaperContainers.Count} taper stacks for delta detection");
- WriteToChat($"Known stack sizes: {lastKnownStackSizes.Count} items");
+ 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!");
diff --git a/MosswartMassacre/RareTracker.cs b/MosswartMassacre/RareTracker.cs
new file mode 100644
index 0000000..5bd7c9d
--- /dev/null
+++ b/MosswartMassacre/RareTracker.cs
@@ -0,0 +1,71 @@
+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;
+ }
+ }
+}