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