diff --git a/MosswartMassacre/ChestLooter.cs b/MosswartMassacre/ChestLooter.cs
new file mode 100644
index 0000000..a91c619
--- /dev/null
+++ b/MosswartMassacre/ChestLooter.cs
@@ -0,0 +1,1346 @@
+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
new file mode 100644
index 0000000..517925a
--- /dev/null
+++ b/MosswartMassacre/ChestLooterSettings.cs
@@ -0,0 +1,122 @@
+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/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 1882e76..7a5d24a 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -119,7 +119,8 @@ namespace MosswartMassacre
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;
private static Timer questStreamingTimer;
@@ -215,6 +216,8 @@ namespace MosswartMassacre
// Initialize navigation visualization system
navVisualization = new NavVisualization();
+ // 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
@@ -329,6 +332,21 @@ namespace MosswartMassacre
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}");
+ }
+
// Apply the values
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
@@ -494,6 +512,21 @@ namespace MosswartMassacre
// 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) =>
+ {
+ VVSTabbedMainView.UpdateChestLooterStatus(status);
+ };
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
+ }
+
// 2. Apply the values from settings
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
@@ -1216,7 +1249,7 @@ namespace MosswartMassacre
}
else
{
- // Hot reload fallback - use CoreManager directly like the original template
+ // Hot reload fallback1 - use CoreManager directly like the original template
CoreManager.Current.Actions.AddChatText("[Mosswart Massacre] " + message, 1);
}
}
@@ -1277,7 +1310,7 @@ namespace MosswartMassacre
}
private void HandleMmCommand(string text)
{
- // Remove the /mm prefix and trim extra whitespace
+ // Remove the /mm prefix and trim extra whitespace test
string[] args = text.Substring(3).Trim().Split(' ');
if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
@@ -1356,6 +1389,10 @@ namespace MosswartMassacre
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");
+ WriteToChat("/mm setchest - Set chest name for looter");
+ WriteToChat("/mm setkey - Set key name for looter");
+ WriteToChat("/mm lootchest - Start chest looting");
+ WriteToChat("/mm stoploot - Stop chest looting");
WriteToChat("/mm nextwp - Advance VTank to next waypoint");
WriteToChat("/mm decalstatus - Check Harmony patch status (UtilityBelt version)");
WriteToChat("/mm decaldebug - Enable/disable plugin message debug output + WebSocket streaming");
@@ -1460,6 +1497,69 @@ namespace MosswartMassacre
}
break;
+ case "setchest":
+ 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();
+ }
+ break;
+
+ case "setkey":
+ 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();
+ }
+ break;
+
+ case "lootchest":
+ if (chestLooter != null)
+ {
+ if (!chestLooter.StartByName())
+ {
+ WriteToChat("[ChestLooter] Failed to start. Check chest/key names are set.");
+ }
+ }
+ else
+ {
+ WriteToChat("[ChestLooter] Chest looter not initialized");
+ }
+ break;
+
+ case "stoploot":
+ if (chestLooter != null)
+ {
+ chestLooter.Stop();
+ }
+ else
+ {
+ WriteToChat("[ChestLooter] Chest looter not initialized");
+ }
+ break;
+
case "vtanktest":
try
{
diff --git a/MosswartMassacre/ViewXML/mainViewTabbed.xml b/MosswartMassacre/ViewXML/mainViewTabbed.xml
index 0707aae..2e90591 100644
--- a/MosswartMassacre/ViewXML/mainViewTabbed.xml
+++ b/MosswartMassacre/ViewXML/mainViewTabbed.xml
@@ -123,6 +123,37 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file