diff --git a/MosswartMassacre/FlagTrackerData.cs b/MosswartMassacre/FlagTrackerData.cs new file mode 100644 index 0000000..2db33b5 --- /dev/null +++ b/MosswartMassacre/FlagTrackerData.cs @@ -0,0 +1,2008 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Decal.Adapter; +using Decal.Adapter.Wrappers; +using Decal.Filters; +using Mag.Shared.Constants; + +namespace MosswartMassacre +{ + /// + /// Data management class for Flag Tracker + /// Ported from UBS Lua flagtracker data structures + /// + public class FlagTrackerData : IDisposable + { + public FlagTrackerData() + { + InitializeDataStructures(); + } + #region Augmentation Data Structures + public class AugmentationInfo + { + public string Name { get; set; } + public int? IntId { get; set; } + public int Repeatable { get; set; } + public string Trainer { get; set; } + public string Location { get; set; } + public int CurrentValue { get; set; } + public bool IsMaxed => CurrentValue >= Repeatable; + } + + public Dictionary> AugmentationCategories { get; private set; } + + #endregion + + #region Luminance Aura Data Structures + public class LuminanceAuraInfo + { + public string Name { get; set; } + public int IntId { get; set; } + public int Cap { get; set; } + public string QuestFlag { get; set; } // For Seer auras only + public int CurrentValue { get; set; } + } + + public Dictionary> LuminanceAuraCategories { get; private set; } + #endregion + + #region Recall Spell Data Structures + public class RecallSpellInfo + { + public string Name { get; set; } + public int SpellId { get; set; } + public bool IsKnown { get; set; } + public int IconId { get; set; } + public string Category { get; set; } + } + + public List RecallSpells { get; private set; } + #endregion + + #region Society Quest Data Structures + public class SocietyQuestInfo + { + public string Name { get; set; } + public string StartTag { get; set; } + public string EndTag { get; set; } + public QuestType Type { get; set; } + public object[] ExtraData { get; set; } // For type-specific data + } + + public enum QuestType + { + Other = 0, + KillTask = 1, + CollectItem = 2, + QuestTag = 3, + MultiQuestTag = 4 + } + + public Dictionary> SocietyQuests { get; private set; } + #endregion + + #region Character Flag Data Structures + public class CharacterFlagInfo + { + public string Name { get; set; } + public string QuestFlag { get; set; } + public QuestInfoType InfoType { get; set; } + } + + public enum QuestInfoType + { + SolveCount = 1, + ReadyCheck = 2, + StampCheck = 3 + } + + public Dictionary> CharacterFlags { get; private set; } + #endregion + + #region Cantrip Data Structures + public class CantripInfo + { + public string Name { get; set; } + public string Value { get; set; } // "Minor", "Major", "Epic", etc. + public System.Drawing.Color Color { get; set; } + public int? IconId { get; set; } // Skill icon from character skills + public int? SpellIconId { get; set; } // Spell icon (for attributes) + public int? BackgroundIconId { get; set; } // Background icon (for attributes) + public int ComputedIconId { get; set; } // Final resolved icon for display + } + + public Dictionary> Cantrips { get; private set; } + + // Skill name mappings for cantrips that have different names than their skills + private readonly Dictionary SkillCantripReplacements = new Dictionary + { + [15] = "MagicResistance", // MagicDefense + [6] = "Invulnerability", // MeleeDefense + [7] = "Impgrenability", // MissileDefense + [47] = "MissileWeapon", // MissileWeapons + [44] = "HeavyWeapon", // HeavyWeapons + [45] = "LightWeapon", // LightWeapons + [46] = "FinesseWeapon" // FinesseWeapons + }; + #endregion + + #region Weapon Data Structures (Legacy - to be removed) + // Old weapon structure - keeping for compatibility but unused + #endregion + + #region Cached Data + private DateTime lastUpdateTime = DateTime.MinValue; + private Dictionary cachedValues = new Dictionary(); + #endregion + + #region Initialization + private void InitializeDataStructures() + { + InitializeAugmentationData(); + InitializeLuminanceAuraData(); + InitializeRecallSpellData(); + InitializeSocietyQuestData(); + InitializeCharacterFlagData(); + InitializeCantripData(); + InitializeNewWeaponData(); + } + + private void InitializeAugmentationData() + { + AugmentationCategories = new Dictionary> + { + ["Death Augs"] = new List + { + new AugmentationInfo { Name = "Keep Items", IntId = 231, Repeatable = 3, Trainer = "Rohula bint Ludun", Location = "Ayan Baqur" }, // AugmentationLessDeathItemLoss + new AugmentationInfo { Name = "Keep Spells", IntId = 232, Repeatable = 1, Trainer = "Erik Festus", Location = "Ayan Baqur" } // AugmentationSpellsRemainPastDeath + }, + ["Skill Augs"] = new List + { + new AugmentationInfo { Name = "+5 All Skills", IntId = 326, Repeatable = 1, Trainer = "Arianna the Adept", Location = "Bandit Castle" }, // AugmentationJackOfAllTrades + new AugmentationInfo { Name = "+10 Melee Skills", IntId = 300, Repeatable = 1, Trainer = "Carlito Gallo", Location = "Silyun" }, // AugmentationSkilledMelee + new AugmentationInfo { Name = "+10 Magic Skills", IntId = 302, Repeatable = 1, Trainer = "Rahina bint Zalanis", Location = "Zaikhal" }, // AugmentationSkilledMagic + new AugmentationInfo { Name = "+10 Missile Skills", IntId = 301, Repeatable = 1, Trainer = "Kilaf", Location = "Zaikhal" } // AugmentationSkilledMissile + }, + ["Rating Augs"] = new List + { + new AugmentationInfo { Name = "25% Crit Protection", IntId = 233, Repeatable = 1, Trainer = "Piersanti Linante", Location = "Sanamar" }, // AugmentationCriticalDefense + new AugmentationInfo { Name = "1% Critical Chance", IntId = 298, Repeatable = 1, Trainer = "Anfram Mellow", Location = "Ayan Baqur" }, // AugmentationCriticalExpertise + new AugmentationInfo { Name = "3% Critical Damage", IntId = 299, Repeatable = 1, Trainer = "Alishia bint Aldan", Location = "Ayan Baqur" }, // AugmentationCriticalPower + new AugmentationInfo { Name = "3% Damage Rating", IntId = 309, Repeatable = 1, Trainer = "Neela Nashua", Location = "Bandit Castle" }, // AugmentationDamageBonus + new AugmentationInfo { Name = "3% Damage Reduction", IntId = 310, Repeatable = 1, Trainer = "Emily Yarow", Location = "Cragstone" } // AugmentationDamageReduction + }, + ["Burden / Pack Augs"] = new List + { + new AugmentationInfo { Name = "Extra Carrying Capacity", IntId = 230, Repeatable = 5, Trainer = "Husoon", Location = "Zaikhal" }, // AugmentationIncreasedCarryingCapacity + new AugmentationInfo { Name = "Extra Pack Slot", IntId = 229, Repeatable = 1, Trainer = "Dumida bint Ruminre", Location = "Zaikhal" }, // AugmentationExtraPackSlot + new AugmentationInfo { Name = "Infused War Magic", IntId = 297, Repeatable = 1, Trainer = "Raphel Detante", Location = "Silyun" }, // AugmentationInfusedWarMagic + new AugmentationInfo { Name = "Infused Void Magic", IntId = 328, Repeatable = 1, Trainer = "Morathe", Location = "Candeth Keep" }, // AugmentationInfusedVoidMagic + new AugmentationInfo { Name = "Infused Creature Magic", IntId = 294, Repeatable = 1, Trainer = "Gustuv Lansdown", Location = "Cragstone" }, // AugmentationInfusedCreatureMagic + new AugmentationInfo { Name = "Infused Life Magic", IntId = 296, Repeatable = 1, Trainer = "Akemi Fei", Location = "Hebian-To" }, // AugmentationInfusedLifeMagic + new AugmentationInfo { Name = "Infused Item Magic", IntId = 295, Repeatable = 1, Trainer = "Gan Fo", Location = "Hebian-To" } // AugmentationInfusedItemMagic + }, + ["Misc Augs"] = new List + { + new AugmentationInfo { Name = "10% Health Increase", IntId = null, Repeatable = 1, Trainer = "Donatello Linante", Location = "Silyun" }, // No specific property + new AugmentationInfo { Name = "Increased Spell Duration", IntId = 238, Repeatable = 5, Trainer = "Nawamara Ujio", Location = "Mayoi" }, // AugmentationIncreasedSpellDuration + new AugmentationInfo { Name = "Faster HP Regen", IntId = 237, Repeatable = 2, Trainer = "Alison Dulane", Location = "Bandit Castle" }, // AugmentationFasterRegen + new AugmentationInfo { Name = "5% Experience Increase", IntId = 234, Repeatable = 1, Trainer = "Rickard Dumalia", Location = "Silyun" } // AugmentationBonusXp + }, + ["Salvage Augs"] = new List + { + new AugmentationInfo { Name = "Specialized Weapon Tinkering", IntId = 228, Repeatable = 1, Trainer = "Lenor Turk", Location = "Cragstone" }, // AugmentationSpecializeWeaponTinkering + new AugmentationInfo { Name = "Specialized Armor Tinkering", IntId = 226, Repeatable = 1, Trainer = "Joshun Felden", Location = "Cragstone" }, // AugmentationSpecializeArmorTinkering + new AugmentationInfo { Name = "Specialized Item Tinkering", IntId = 225, Repeatable = 1, Trainer = "Brienne Carlus", Location = "Cragstone" }, // AugmentationSpecializeItemTinkering + new AugmentationInfo { Name = "Specialized Magic Item Tinkering", IntId = 227, Repeatable = 1, Trainer = "Burrell Sammrun", Location = "Cragstone" }, // AugmentationSpecializeMagicItemTinkering + new AugmentationInfo { Name = "Specialized Salvaging", IntId = 224, Repeatable = 1, Trainer = "Robert Crow", Location = "Cragstone" }, // AugmentationSpecializeSalvaging + new AugmentationInfo { Name = "25% More Salvage", IntId = 235, Repeatable = 4, Trainer = "Kris Cennis", Location = "Cragstone" }, // AugmentationBonusSalvage + new AugmentationInfo { Name = "5% Imbue Chance", IntId = 236, Repeatable = 1, Trainer = "Lug", Location = "Oolutanga's Refuge" } // AugmentationBonusImbueChance + }, + ["Stat Augs"] = new List + { + new AugmentationInfo { Name = "All Stats", IntId = 217, Repeatable = 10, Trainer = "", Location = "" }, // AugmentationInnateFamily + new AugmentationInfo { Name = "Strength", IntId = 218, Repeatable = 10, Trainer = "Fiun Luunere", Location = "Fiun Outpost" }, // AugmentationInnateStrength + new AugmentationInfo { Name = "Endurance", IntId = 219, Repeatable = 10, Trainer = "Fiun Ruun", Location = "Fiun Outpost" }, // AugmentationInnateEndurance + new AugmentationInfo { Name = "Coordination", IntId = 220, Repeatable = 10, Trainer = "Fiun Bayaas", Location = "Fiun Outpost" }, // AugmentationInnateCoordination + new AugmentationInfo { Name = "Quickness", IntId = 221, Repeatable = 10, Trainer = "Fiun Riish", Location = "Fiun Outpost" }, // AugmentationInnateQuickness + new AugmentationInfo { Name = "Focus", IntId = 222, Repeatable = 10, Trainer = "Fiun Vasherr", Location = "Fiun Outpost" }, // AugmentationInnateFocus + new AugmentationInfo { Name = "Self", IntId = 223, Repeatable = 10, Trainer = "Fiun Noress", Location = "Fiun Outpost" } // AugmentationInnateSelf + }, + ["Resistance Augs"] = new List + { + new AugmentationInfo { Name = "All Resistances", IntId = 239, Repeatable = 2, Trainer = "", Location = "" }, // AugmentationResistanceFamily + new AugmentationInfo { Name = "Blunt", IntId = 242, Repeatable = 2, Trainer = "Nawamara Dia", Location = "Hebian-To" }, // AugmentationResistanceBlunt + new AugmentationInfo { Name = "Pierce", IntId = 241, Repeatable = 2, Trainer = "Kyujo Rujen", Location = "Hebian-To" }, // AugmentationResistancePierce + new AugmentationInfo { Name = "Slashing", IntId = 240, Repeatable = 2, Trainer = "Ilin Wis", Location = "Hebian-To" }, // AugmentationResistanceSlash + new AugmentationInfo { Name = "Fire", IntId = 244, Repeatable = 2, Trainer = "Rikshen Ri", Location = "Hebian-To" }, // AugmentationResistanceFire + new AugmentationInfo { Name = "Frost", IntId = 245, Repeatable = 2, Trainer = "Lu Bao", Location = "Hebian-To" }, // AugmentationResistanceFrost + new AugmentationInfo { Name = "Acid", IntId = 243, Repeatable = 2, Trainer = "Shujio Milao", Location = "Hebian-To" }, // AugmentationResistanceAcid + new AugmentationInfo { Name = "Lightning", IntId = 246, Repeatable = 2, Trainer = "Enli Yuo", Location = "Hebian-To" } // AugmentationResistanceLightning + } + }; + } + + private void InitializeLuminanceAuraData() + { + LuminanceAuraCategories = new Dictionary> + { + ["Nalicana Auras"] = new List + { + new LuminanceAuraInfo { Name = "+1 Aetheria Proc Rating", IntId = 338, Cap = 5 }, // LumAugSurgeChanceRating + new LuminanceAuraInfo { Name = "+1 Damage Reduction Rating", IntId = 334, Cap = 5 }, // LumAugDamageReductionRating + new LuminanceAuraInfo { Name = "+1 Crit Reduction Rating", IntId = 336, Cap = 5 }, // LumAugCritReductionRating + new LuminanceAuraInfo { Name = "+1 Damage Rating", IntId = 333, Cap = 5 }, // LumAugDamageRating + new LuminanceAuraInfo { Name = "+1 Crit Damage Rating", IntId = 335, Cap = 5 }, // LumAugCritDamageRating + new LuminanceAuraInfo { Name = "+1 Heal Rating", IntId = 342, Cap = 5 }, // LumAugHealingRating + new LuminanceAuraInfo { Name = "+1 Equipment Mana Rating", IntId = 339, Cap = 5 }, // LumAugItemManaUsage + new LuminanceAuraInfo { Name = "+1 Mana Stone Rating", IntId = 340, Cap = 5 }, // LumAugItemManaGain + new LuminanceAuraInfo { Name = "+1 Crafting Skills", IntId = 343, Cap = 5 }, // LumAugSkilledCraft + new LuminanceAuraInfo { Name = "+1 All Skills", IntId = 365, Cap = 10 } // LumAugAllSkills + }, + ["Seer Auras"] = new List + { + new LuminanceAuraInfo { Name = "(Ka'hiri) +2 Specialized Skills", IntId = 344, Cap = 5, QuestFlag = "LoyalToKahiri" }, // LumAugSkilledSpec + new LuminanceAuraInfo { Name = "(Ka'hiri) +1 Damage Rating", IntId = 333, Cap = 5, QuestFlag = "LoyalToKahiri" }, // LumAugDamageRating + new LuminanceAuraInfo { Name = "(Shade of Lady Adja) +2 Specialized Skills", IntId = 344, Cap = 5, QuestFlag = "LoyalToShadeOfLadyAdja" }, // LumAugSkilledSpec + new LuminanceAuraInfo { Name = "(Shade of Lady Adja) +1 Damage Reduction Rating", IntId = 334, Cap = 5, QuestFlag = "LoyalToShadeOfLadyAdja" }, // LumAugDamageReductionRating + new LuminanceAuraInfo { Name = "(Liam of Gelid) +1 Damage Rating", IntId = 333, Cap = 5, QuestFlag = "LoyalToLiamOfGelid" }, // LumAugDamageRating + new LuminanceAuraInfo { Name = "(Liam of Gelid) +1 Crit Damage Rating", IntId = 335, Cap = 5, QuestFlag = "LoyalToLiamOfGelid" }, // LumAugCritDamageRating + new LuminanceAuraInfo { Name = "(Lord Tyragar) +1 Crit Reduction Rating", IntId = 336, Cap = 5, QuestFlag = "LoyalToLordTyragar" }, // LumAugCritReductionRating + new LuminanceAuraInfo { Name = "(Lord Tyragar) +1 Damage Reduction Rating", IntId = 334, Cap = 5, QuestFlag = "LoyalToLordTyragar" } // LumAugDamageReductionRating + } + }; + } + + private void InitializeRecallSpellData() + { + RecallSpells = new List + { + new RecallSpellInfo { Name = "Recall the Sanctuary", SpellId = 2023, IconId = 0, Category = "Basic Recalls" }, + new RecallSpellInfo { Name = "Aerlinthe Recall", SpellId = 2041, IconId = 0, Category = "Island Recalls" }, + new RecallSpellInfo { Name = "Mount Lethe Recall", SpellId = 2813, IconId = 0, Category = "Island Recalls" }, + new RecallSpellInfo { Name = "Recall Aphus Lassel", SpellId = 2931, IconId = 0, Category = "Island Recalls" }, + new RecallSpellInfo { Name = "Ulgrim's Recall", SpellId = 2941, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Recall to the Singularity Caul", SpellId = 2943, IconId = 0, Category = "Island Recalls" }, + new RecallSpellInfo { Name = "Glenden Wood Recall", SpellId = 3865, IconId = 0, Category = "Town Recalls" }, + new RecallSpellInfo { Name = "Bur Recall", SpellId = 4084, IconId = 0, Category = "Town Recalls" }, + new RecallSpellInfo { Name = "Call of the Mhoire Forge", SpellId = 4128, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Paradox-touched Olthoi Infested Area Recall", SpellId = 4198, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Colosseum Recall", SpellId = 4213, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Return to the Keep", SpellId = 4214, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Gear Knight Invasion Area Camp Recall", SpellId = 5330, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Lost City of Neftet Recall", SpellId = 5541, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Rynthid Recall", SpellId = 6150, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Viridian Rise Recall", SpellId = 6321, IconId = 0, Category = "Special Recalls" }, + new RecallSpellInfo { Name = "Viridian Rise Great Tree Recall", SpellId = 6322, IconId = 0, Category = "Special Recalls" } + }; + } + + private void InitializeSocietyQuestData() + { + SocietyQuests = new Dictionary>(); + // TODO: Initialize society quest data from Lua + } + + private void InitializeCharacterFlagData() + { + CharacterFlags = new Dictionary> + { + ["Additional Skill Credits"] = new List + { + new CharacterFlagInfo { Name = "+1 Skill Lum Aura", QuestFlag = "lumaugskillquest", InfoType = QuestInfoType.SolveCount }, + new CharacterFlagInfo { Name = "+1 Skill Aun Ralirea", QuestFlag = "arantahkill1", InfoType = QuestInfoType.SolveCount }, + new CharacterFlagInfo { Name = "+1 Skill Chasing Oswald", QuestFlag = "oswaldmanualcompleted", InfoType = QuestInfoType.SolveCount } + }, + ["Aetheria"] = new List + { + new CharacterFlagInfo { Name = "Blue Aetheria (75)", QuestFlag = "efulcentermanafieldused", InfoType = QuestInfoType.StampCheck }, + new CharacterFlagInfo { Name = "Yellow Aetheria (150)", QuestFlag = "efmlcentermanafieldused", InfoType = QuestInfoType.StampCheck }, + new CharacterFlagInfo { Name = "Red Aetheria (225)", QuestFlag = "efllcentermanafieldused", InfoType = QuestInfoType.StampCheck } + } + // TODO: Add remaining character flag categories + }; + } + + private void InitializeCantripData() + { + Cantrips = new Dictionary> + { + ["Specialized Skills"] = new Dictionary(), // Dynamically populated + ["Trained Skills"] = new Dictionary(), // Dynamically populated + ["Attributes"] = new Dictionary(), // Dynamically populated when cantrips are detected + ["Protection Auras"] = new Dictionary + { + // Pre-populate all protection auras so they show as red when missing + ["Armor"] = new CantripInfo { Name = "Armor", Value = "N/A", Color = System.Drawing.Color.White }, + ["Bludgeoning Ward"] = new CantripInfo { Name = "Bludgeoning Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Piercing Ward"] = new CantripInfo { Name = "Piercing Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Slashing Ward"] = new CantripInfo { Name = "Slashing Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Flame Ward"] = new CantripInfo { Name = "Flame Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Frost Ward"] = new CantripInfo { Name = "Frost Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Acid Ward"] = new CantripInfo { Name = "Acid Ward", Value = "N/A", Color = System.Drawing.Color.White }, + ["Storm Ward"] = new CantripInfo { Name = "Storm Ward", Value = "N/A", Color = System.Drawing.Color.White } + } + }; + } + + #endregion + + #region Refresh Methods + public void RefreshAll() + { + RefreshCachedData(); + RefreshAugmentations(); + RefreshLuminanceAuras(); + RefreshRecallSpells(); + RefreshSocietyQuests(); + RefreshFacilityHubQuests(); + RefreshCharacterFlags(); + RefreshCantrips(); + } + + public void RefreshCachedData() + { + try + { + if (CoreManager.Current?.CharacterFilter?.Name != null) + { + // Update cached values + var character = CoreManager.Current.CharacterFilter; + lastUpdateTime = DateTime.Now; + + // TODO: Implement cached data refresh + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing cached data: {ex.Message}"); + } + } + + public void RefreshAugmentations() + { + try + { + if (CoreManager.Current?.CharacterFilter?.Name == null) return; + + var character = CoreManager.Current.CharacterFilter; + + // Update augmentation values + foreach (var category in AugmentationCategories) + { + foreach (var aug in category.Value) + { + if (aug.IntId.HasValue) + { + // Get augmentation value from character data using DECAL API + try + { + if (CoreManager.Current?.CharacterFilter != null) + { + var characterFilter = CoreManager.Current.CharacterFilter; + var playerObject = CoreManager.Current.WorldFilter[characterFilter.Id]; + + if (playerObject != null) + { + // Use CharacterFilter.GetCharProperty for character properties + try + { + // DECAL API uses CharacterFilter.GetCharProperty for character properties + aug.CurrentValue = characterFilter.GetCharProperty(aug.IntId.Value); + // Debug disabled: PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} (Property: {aug.IntId.Value})"); + } + catch (Exception ex) + { + // Try alternative access using reflection + try + { + var valuesMethod = playerObject.GetType().GetMethod("Values", new Type[] { typeof(int) }); + if (valuesMethod != null) + { + aug.CurrentValue = (int)valuesMethod.Invoke(playerObject, new object[] { aug.IntId.Value }); + PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} via reflection (ID: {aug.IntId.Value})"); + } + else + { + aug.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aug.Name} - Values method not found"); + } + } + catch (Exception ex2) + { + aug.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aug.Name} - Failed: {ex.Message}, Reflection: {ex2.Message}"); + } + } + } + else + { + aug.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aug.Name} - Player object is null"); + } + } + else + { + aug.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aug.Name} - CharacterFilter is null"); + } + } + catch (Exception ex) + { + aug.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aug.Name} - Exception: {ex.Message}"); + } + } + else + { + // Handle special case for Asheron's Lesser Benediction (inventory count) + if (aug.Name == "Asheron's Lesser Benediction") + { + // Count Asheron's Lesser Benediction items in inventory + aug.CurrentValue = CountAsheronsLesserBenediction(); + } + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing augmentations: {ex.Message}"); + } + } + + private int CountAsheronsLesserBenediction() + { + try + { + int count = 0; + + // Search inventory for Asheron's Lesser Benediction + foreach (WorldObject item in CoreManager.Current.WorldFilter.GetInventory()) + { + if (item.Name.Contains("Asheron's Lesser Benediction")) + { + // Use stack size for item count (default to 1 for non-stackable items) + try + { + // Access stack size using reflection to avoid type issues + var stackMethod = item.GetType().GetMethod("Values", new Type[] { typeof(int) }); + if (stackMethod != null) + { + int stackSize = (int)stackMethod.Invoke(item, new object[] { 12 }); // 12 = StackSize + count += Math.Max(1, stackSize); + } + else + { + count += 1; // Default to 1 + } + } + catch + { + count += 1; // Default fallback + } + } + } + + return count; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error counting Asheron's Lesser Benediction: {ex.Message}"); + return 0; + } + } + + public void RefreshLuminanceAuras() + { + try + { + if (CoreManager.Current?.CharacterFilter?.Name == null) return; + + var characterFilter = CoreManager.Current.CharacterFilter; + + // Update luminance aura values + foreach (var category in LuminanceAuraCategories) + { + foreach (var aura in category.Value) + { + try + { + // Use CharacterFilter.GetCharProperty for luminance auras + aura.CurrentValue = characterFilter.GetCharProperty(aura.IntId); + // Debug disabled: PluginCore.WriteToChat($"Debug: {aura.Name} = {aura.CurrentValue}/{aura.Cap} (Property: {aura.IntId})"); + } + catch (Exception ex) + { + aura.CurrentValue = 0; + PluginCore.WriteToChat($"Debug: {aura.Name} - Failed to read: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing luminance auras: {ex.Message}"); + } + } + + public void RefreshRecallSpells() + { + try + { + if (CoreManager.Current?.CharacterFilter?.Name == null) return; + + var characterFilter = CoreManager.Current.CharacterFilter; + int knownCount = 0; + + // Check each recall spell to see if the character knows it + foreach (var recall in RecallSpells) + { + try + { + // Use DECAL API to check if the character knows this spell + recall.IsKnown = characterFilter.IsSpellKnown(recall.SpellId); + if (recall.IsKnown) knownCount++; + + // Get spell icon from FileService if not already set + if (recall.IconId == 0) + { + recall.IconId = GetSpellIcon(recall.SpellId); + } + } + catch (Exception ex) + { + recall.IsKnown = false; + PluginCore.WriteToChat($"Debug: Error checking {recall.Name}: {ex.Message}"); + } + } + + // PluginCore.WriteToChat($"Debug: Recall spells refresh completed - {knownCount}/{RecallSpells.Count} known"); // Debug disabled + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing recall spells: {ex.Message}"); + } + } + + private int GetSpellIcon(int spellId) + { + try + { + // Try to get real spell icon first (matches original Lua approach) + int realSpellIcon = GetRealSpellIcon(spellId); + if (realSpellIcon != 0) + { + return realSpellIcon; + } + + // Fallback to known recall spell icons + var recallSpellIcons = new Dictionary + { + // Recall spell icons from AC spell data + [2023] = 2943, // Recall the Sanctuary + [2041] = 2814, // Aerlinthe Recall + [2813] = 2813, // Mount Lethe Recall + [2931] = 2931, // Recall Aphus Lassel + [2943] = 2943, // Recall to the Singularity Caul + [3865] = 3864, // Glenden Wood Recall + [4084] = 4084, // Bur Recall + [2941] = 2814, // Ulgrim's Recall (uses portal icon) + [4128] = 4128, // Call of the Mhoire Forge + [4198] = 4197, // Paradox-touched Olthoi Infested Area Recall + [4213] = 4213, // Colosseum Recall + [4214] = 4199, // Return to the Keep + [5330] = 5175, // Gear Knight Invasion Area Camp Recall + [5541] = 5541, // Lost City of Neftet Recall + [6150] = 6150, // Rynthid Recall + [6321] = 6321, // Viridian Rise Recall + [6322] = 6322 // Viridian Rise Great Tree Recall + }; + + if (recallSpellIcons.ContainsKey(spellId)) + { + // Add offset for spell icons + return recallSpellIcons[spellId] + 0x6000000; + } + + // Final fallback - default portal/recall icon + return 0x6002D14; + } + catch + { + return 0x6002D14; // Default icon on error + } + } + + private int GetRealSpellIcon(int spellId) + { + try + { + // Method 1: Use DECAL FileService SpellTable directly (proper API approach) + try + { + var fileService = CoreManager.Current.Filter(); + if (fileService?.SpellTable != null) + { + var spell = fileService.SpellTable.GetById(spellId); + if (spell != null) + { + // Use reflection to access the internal Spell_Class object + // DECAL's Spell wrapper has an internal m_pSpell field that contains the actual data + var spellType = spell.GetType(); + + // Try to get the internal spell object first + var internalSpellField = spellType.GetField("m_pSpell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (internalSpellField != null) + { + var internalSpell = internalSpellField.GetValue(spell); + if (internalSpell != null) + { + // Now get the icon from the internal spell object + var internalType = internalSpell.GetType(); + + // Try icon properties on the internal object + string[] iconPropertyNames = { "Icon", "icon", "IconId", "iconId", "IconID", "iconID" }; + foreach (var propName in iconPropertyNames) + { + try + { + // Try as property + var iconProperty = internalType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconProperty != null) + { + var iconValue = iconProperty.GetValue(internalSpell, null); + if (iconValue is int iconInt && iconInt > 0) + { + // Spell icons use raw values (no offset) + return iconInt; + } + else if (iconValue is uint iconUint && iconUint > 0) + { + return (int)iconUint; + } + } + + // Try as field + var iconField = internalType.GetField(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconField != null) + { + var iconValue = iconField.GetValue(internalSpell); + if (iconValue is int iconInt && iconInt > 0) + { + return iconInt; + } + else if (iconValue is uint iconUint && iconUint > 0) + { + return (int)iconUint; + } + } + } + catch + { + // Continue trying other property names + } + } + } + } + + // Fallback: Try direct properties on the wrapper + string[] wrapperPropertyNames = { "Icon", "IconId", "IconID" }; + foreach (var propName in wrapperPropertyNames) + { + var iconProperty = spellType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance); + if (iconProperty != null) + { + var iconValue = iconProperty.GetValue(spell, null); + if (iconValue is int iconId && iconId > 0) + { + return iconId; + } + } + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: GetRealSpellIcon FileService error: {ex.Message}"); + } + + // Method 2: Use known spell icon mappings for cantrips + // These are based on AC spell icon IDs from spell data + var cantripSpellIcons = new Dictionary + { + // Strength cantrips - use Strength Self VIII icon + [2091] = 1332, // Major Strength + [4325] = 1332, // Epic Strength + [6107] = 1332, // Legendary Strength + + // Endurance cantrips - use Endurance Self VIII icon + [2061] = 1354, // Major Endurance + [4226] = 1354, // Epic Endurance + [6104] = 1354, // Legendary Endurance + + // Coordination cantrips - use Coordination Self VIII icon + [2059] = 1378, // Major Coordination + [4296] = 1378, // Epic Coordination + [6102] = 1378, // Legendary Coordination + + // Quickness cantrips - use Quickness Self VIII icon + [2081] = 1409, // Major Quickness + [4319] = 1409, // Epic Quickness + [6106] = 1409, // Legendary Quickness + + // Focus cantrips - use Focus Self VIII icon + [2067] = 1426, // Major Focus + [4304] = 1426, // Epic Focus + [6105] = 1426, // Legendary Focus + + // Willpower/Self cantrips - use Willpower Self VIII icon + [2091] = 1450, // Major Willpower + [4329] = 1450, // Epic Willpower + [6101] = 1450, // Legendary Willpower + + // Protection Auras - Armor - use Armor Self VIII icon + [2113] = 1316, // Major Armor + [4291] = 1316, // Epic Armor + [6095] = 1316, // Legendary Armor + + // Protection Auras - Physical + [2245] = 1023, // Major Piercing Ward - use Blade Protection Self VIII + [4306] = 1023, // Epic Piercing Ward + [6096] = 1023, // Legendary Piercing Ward + + [2244] = 1114, // Major Slashing Ward - use Piercing Protection Self VIII + [4321] = 1114, // Epic Slashing Ward + [6097] = 1114, // Legendary Slashing Ward + + [2243] = 1138, // Major Bludgeoning Ward - use Bludgeoning Protection Self VIII + [4293] = 1138, // Epic Bludgeoning Ward + [6098] = 1138, // Legendary Bludgeoning Ward + + // Protection Auras - Elemental + [2157] = 1096, // Major Frost Ward - use Cold Protection Self VIII + [4309] = 1096, // Epic Frost Ward + [6100] = 1096, // Legendary Frost Ward + + [2158] = 1035, // Major Flame Ward - use Fire Protection Self VIII + [4294] = 1035, // Epic Flame Ward + [6099] = 1035, // Legendary Flame Ward + + [2149] = 1078, // Major Acid Ward - use Acid Protection Self VIII + [4290] = 1078, // Epic Acid Ward + [6094] = 1078, // Legendary Acid Ward + + [2159] = 1161, // Major Storm Ward - use Lightning Protection Self VIII + [4322] = 1161, // Epic Storm Ward + [6079] = 1161, // Legendary Storm Ward + + // Magic Defense cantrips - use Magic Resistance Self VIII icon + [2249] = 610, // Major Magic Resistance + [4314] = 610, // Epic Magic Resistance + [6067] = 610, // Legendary Magic Resistance + + // Melee Defense cantrips - use Invulnerability Self VIII icon + [2248] = 562, // Major Invulnerability + [4312] = 562, // Epic Invulnerability + [6051] = 562, // Legendary Invulnerability + + // Missile Defense cantrips - use Impregnability Self VIII icon + [2247] = 1562, // Major Impregnability + [4311] = 1562, // Epic Impregnability + [6055] = 1562, // Legendary Impregnability + + // Life Magic cantrips - use Life Magic Mastery Self VIII icon + [2156] = 610, // Major Life Magic Aptitude + [4700] = 610, // Epic Life Magic Aptitude + [6044] = 610, // Legendary Life Magic Aptitude + + // War Magic cantrips - use War Magic Mastery Self VIII icon + [2183] = 634, // Major War Magic Aptitude + [4715] = 634, // Epic War Magic Aptitude + [6075] = 634, // Legendary War Magic Aptitude + + // Creature Enchantment cantrips - use Creature Enchantment Mastery Self VIII icon + [2215] = 586, // Major Creature Enchantment Aptitude + [4689] = 586, // Epic Creature Enchantment Aptitude + [6042] = 586, // Legendary Creature Enchantment Aptitude + + // Item Enchantment cantrips - use Item Enchantment Mastery Self VIII icon + [2249] = 658, // Major Item Enchantment Aptitude + [4697] = 658, // Epic Item Enchantment Aptitude + [6043] = 658, // Legendary Item Enchantment Aptitude + + // Void Magic cantrips - use Void Magic Mastery Self VI icon (no VIII version) + [5427] = 5418, // Major Void Magic Aptitude + [5428] = 5418, // Epic Void Magic Aptitude + [5429] = 5418, // Legendary Void Magic Aptitude + + // Weapon cantrips + [2223] = 522, // Major Heavy Weapon Aptitude - use Heavy Weapon Mastery Self VIII + [4624] = 522, // Epic Heavy Weapon Aptitude + [6073] = 522, // Legendary Heavy Weapon Aptitude + + [2226] = 327, // Major Light Weapon Aptitude - use Light Weapon Mastery Self VI + [4639] = 327, // Epic Light Weapon Aptitude + [6074] = 327, // Legendary Light Weapon Aptitude + + [2227] = 350, // Major Finesse Weapon Aptitude - use Finesse Weapon Mastery Self VI + [4638] = 350, // Epic Finesse Weapon Aptitude + [6072] = 350, // Legendary Finesse Weapon Aptitude + + [2230] = 473, // Major Missile Weapon Aptitude - use Missile Weapon Mastery Self VI + [4713] = 473, // Epic Missile Weapon Aptitude + [6071] = 473, // Legendary Missile Weapon Aptitude + + // Mana Conversion cantrips - use Mana Conversion Mastery Self VIII icon + [2152] = 658, // Major Mana Conversion Prowess + [4705] = 658, // Epic Mana Conversion Prowess + [6048] = 658, // Legendary Mana Conversion Prowess + }; + + if (cantripSpellIcons.ContainsKey(spellId)) + { + int iconId = cantripSpellIcons[spellId]; + // Add offset for spell icons to display correctly in VVS + // Based on MagTools pattern, spell icons need the offset for display + int finalIconId = iconId + 0x6000000; + PluginCore.WriteToChat($"Debug: Found cantrip spell {spellId} -> raw icon {iconId} -> final icon 0x{finalIconId:X}"); + return finalIconId; + } + + return 0; // No real icon found + } + catch + { + return 0; + } + } + + public void RefreshSocietyQuests() + { + try + { + // TODO: Implement society quest refresh + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing society quests: {ex.Message}"); + } + } + + public void RefreshFacilityHubQuests() + { + try + { + // TODO: Implement facility hub quest refresh + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing facility hub quests: {ex.Message}"); + } + } + + public void RefreshCharacterFlags() + { + try + { + // TODO: Implement character flag refresh + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing character flags: {ex.Message}"); + } + } + + public void RefreshCantrips() + { + try + { + PluginCore.WriteToChat("Debug: RefreshCantrips() starting"); + + if (CoreManager.Current?.CharacterFilter?.Name == null) + { + PluginCore.WriteToChat("Debug: No character filter available"); + return; + } + + var characterFilter = CoreManager.Current.CharacterFilter; + var playerObject = CoreManager.Current.WorldFilter[characterFilter.Id]; + + if (playerObject == null) + { + PluginCore.WriteToChat("Debug: No player object found"); + return; + } + + PluginCore.WriteToChat($"Debug: Character {characterFilter.Name} found, {playerObject.ActiveSpellCount} active spells"); + + // Clear dynamic skill lists + Cantrips["Specialized Skills"].Clear(); + Cantrips["Trained Skills"].Clear(); + + // Populate skills dynamically based on character's actual training + PopulateCharacterSkills(characterFilter); + + // Reset all cantrips to "N/A" + foreach (var category in Cantrips) + { + foreach (var cantrip in category.Value.Values) + { + cantrip.Value = "N/A"; + cantrip.Color = System.Drawing.Color.White; + } + } + + // Scan active spells for cantrips using CharacterFilter.Enchantments + var enchantments = characterFilter.Enchantments; + if (enchantments != null) + { + PluginCore.WriteToChat($"Debug: Found {enchantments.Count} active enchantments"); + for (int i = 0; i < enchantments.Count; i++) + { + var ench = enchantments[i]; + var spell = SpellManager.GetSpell(ench.SpellId); + if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None) + { + PluginCore.WriteToChat($"Debug: Found cantrip - Spell {ench.SpellId}: {spell.Name} (Level: {spell.CantripLevel})"); + DetectCantrip(ench.SpellId); + } + } + } + else + { + PluginCore.WriteToChat("Debug: No enchantments to scan"); + } + + // Debug: Always show spell info regardless of count + // Compute final icon IDs for all cantrips after refresh + foreach (var category in Cantrips) + { + foreach (var cantrip in category.Value.Values) + { + cantrip.ComputedIconId = ComputeCantripIcon(cantrip); + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing cantrips: {ex.Message}"); + } + } + + private void PopulateCharacterSkills(CharacterFilter characterFilter) + { + try + { + // Map of skill IDs to skill names - based on DECAL CharFilterSkillType enumeration + // Reference: DECAL API documentation and AC development sources + var skillIdToName = new Dictionary + { + [1] = "Axe", // Retired weapon skill + [2] = "Bow", // Retired weapon skill + [3] = "Crossbow", // Retired weapon skill + [4] = "Dagger", // Retired weapon skill + [5] = "Mace", // Retired weapon skill + [6] = "Melee Defense", // Active defense skill + [7] = "Missile Defense", // Active defense skill + [8] = "Sling", // Retired weapon skill + [9] = "Spear", // Retired weapon skill + [10] = "Staff", // Retired weapon skill + [11] = "Sword", // Retired weapon skill + [12] = "Thrown Weapons", // Retired weapon skill + [13] = "Unarmed Combat", // Retired weapon skill + [14] = "Arcane Lore", // Active magic skill + [15] = "Magic Defense", // Active defense skill + [16] = "Mana Conversion", // Active magic skill + [17] = "Spellcraft", // Unused/Reserved + [18] = "Item Tinkering", // Active tinker skill + [19] = "Assess Person", // Active misc skill + [20] = "Deception", // Active misc skill + [21] = "Healing", // Active misc skill + [22] = "Jump", // Active misc skill + [23] = "Lockpick", // Active misc skill + [24] = "Run", // Active misc skill + [25] = "Awareness", // Unused/Reserved + [26] = "Arms and Armor Repair", // Unused/Reserved + [27] = "Assess Creature", // Active misc skill + [28] = "Weapon Tinkering", // Active tinker skill + [29] = "Armor Tinkering", // Active tinker skill + [30] = "Magic Item Tinkering", // Active tinker skill + [31] = "Creature Enchantment", // Active magic skill + [32] = "Item Enchantment", // Active magic skill + [33] = "Life Magic", // Active magic skill + [34] = "War Magic", // Active magic skill + [35] = "Leadership", // Active misc skill + [36] = "Loyalty", // Active misc skill + [37] = "Fletching", // Active tinker skill + [38] = "Alchemy", // Active tinker skill + [39] = "Cooking", // Active tinker skill + [40] = "Salvaging", // Active tinker skill + [41] = "Two Handed Combat", // Active weapon skill + [42] = "Gearcraft", // Retired tinker skill + [43] = "Void Magic", // Active magic skill + [44] = "Heavy Weapons", // Active weapon skill + [45] = "Light Weapons", // Active weapon skill + [46] = "Finesse Weapons", // Active weapon skill + [47] = "Missile Weapons", // Active weapon skill + [48] = "Shield", // Active weapon skill + [49] = "Dual Wield", // Active weapon skill + [50] = "Recklessness", // Active weapon skill + [51] = "Sneak Attack", // Active weapon skill + [52] = "Dirty Fighting", // Active weapon skill + [53] = "Threat Assessment", // Unused/Reserved + [54] = "Summoning" // Active magic skill + }; + + // Check each skill's training status + foreach (var kvp in skillIdToName) + { + int skillId = kvp.Key; + string skillName = kvp.Value; + + try + { + // Get skill training status using CharacterFilter.Skills + var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId]; + if (skillInfo == null) continue; + + // Apply skill name replacements for cantrips + if (SkillCantripReplacements.ContainsKey(skillId)) + { + skillName = SkillCantripReplacements[skillId]; + } + + if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized) + { + PluginCore.WriteToChat($"Debug: Adding specialized skill: {skillName} (ID {skillId})"); + Cantrips["Specialized Skills"][skillName] = new CantripInfo + { + Name = skillName, + Value = "N/A", + Color = System.Drawing.Color.White + // IconId removed - will be set by spell icon when cantrips are detected + }; + } + else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained) + { + PluginCore.WriteToChat($"Debug: Adding trained skill: {skillName} (ID {skillId})"); + Cantrips["Trained Skills"][skillName] = new CantripInfo + { + Name = skillName, + Value = "N/A", + Color = System.Drawing.Color.White + // IconId removed - will be set by spell icon when cantrips are detected + }; + } + } + catch + { + // Skill not available on this character, skip it + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating character skills: {ex.Message}"); + } + } + + private int? GetSkillIconId(int skillId) + { + try + { + var characterFilter = CoreManager.Current.CharacterFilter; + if (characterFilter == null) + { + PluginCore.WriteToChat($"Debug: CharacterFilter is null for skill {skillId}"); + return GetFallbackSkillIcon(skillId); + } + + // Validate skillId range for DECAL API + if (skillId < 1 || skillId > 54) + { + PluginCore.WriteToChat($"Debug: Invalid skill ID {skillId} (must be 1-54)"); + return GetFallbackSkillIcon(skillId); + } + + try + { + var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId]; + if (skillInfo == null) + { + PluginCore.WriteToChat($"Debug: No skill info found for skill {skillId}"); + return GetFallbackSkillIcon(skillId); + } + + PluginCore.WriteToChat($"Debug: Attempting icon access for skill {skillId} ({GetSkillName(skillId)})"); + + // Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property) + var skillType = skillInfo.GetType(); + PluginCore.WriteToChat($"Debug: SkillInfo type: {skillType.Name}"); + + // Method 1: Try FileService SkillTable approach (most reliable) + int realIconId = GetRealSkillIconFromDat(skillId); + if (realIconId > 0) + { + PluginCore.WriteToChat($"Debug: Found real skill icon {realIconId} for skill {skillId}, applying offset"); + return realIconId + 0x6000000; + } + + // Method 2: Reflection on SkillInfoWrapper.Dat + var datProperty = skillType.GetProperty("Dat", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (datProperty != null) + { + var datObject = datProperty.GetValue(skillInfo, null); + if (datObject != null) + { + var datType = datObject.GetType(); + PluginCore.WriteToChat($"Debug: Dat object type: {datType.Name}"); + + // Try the exact property names from AC system + string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" }; + + foreach (var propName in iconPropertyNames) + { + var iconProperty = datType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconProperty != null) + { + var iconValue = iconProperty.GetValue(datObject, null); + if (iconValue != null) + { + PluginCore.WriteToChat($"Debug: Found {propName} property with value {iconValue} (type: {iconValue.GetType().Name})"); + if (iconValue is int iconId && iconId > 0) + { + PluginCore.WriteToChat($"Debug: Using icon {iconId} from {propName} for skill {skillId}"); + return iconId + 0x6000000; + } + else if (iconValue is uint uiconId && uiconId > 0) + { + PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from {propName} for skill {skillId}"); + return (int)uiconId + 0x6000000; + } + } + } + else + { + // Try as field + var iconField = datType.GetField(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconField != null) + { + var iconValue = iconField.GetValue(datObject); + if (iconValue != null) + { + PluginCore.WriteToChat($"Debug: Found {propName} field with value {iconValue} (type: {iconValue.GetType().Name})"); + if (iconValue is int iconId && iconId > 0) + { + PluginCore.WriteToChat($"Debug: Using icon {iconId} from field {propName} for skill {skillId}"); + return iconId + 0x6000000; + } + else if (iconValue is uint uiconId && uiconId > 0) + { + PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from field {propName} for skill {skillId}"); + return (int)uiconId + 0x6000000; + } + } + } + } + } + + // Debug: List all available properties and fields on Dat object + PluginCore.WriteToChat($"Debug: Available properties on {datType.Name}:"); + foreach (var prop in datType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + try + { + var val = prop.GetValue(datObject, null); + PluginCore.WriteToChat($" {prop.Name}: {val} ({prop.PropertyType.Name})"); + } + catch (Exception ex) + { + PluginCore.WriteToChat($" {prop.Name}: ({prop.PropertyType.Name})"); + } + } + } + else + { + PluginCore.WriteToChat($"Debug: Dat property exists but returns null for skill {skillId}"); + } + } + else + { + PluginCore.WriteToChat($"Debug: No Dat property found on SkillInfoWrapper for skill {skillId}"); + } + + // Method 3: Try direct properties on SkillInfoWrapper + string[] directPropertyNames = { "IconID", "Icon", "IconId", "GraphicID" }; + foreach (var propName in directPropertyNames) + { + var iconProperty = skillType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconProperty != null) + { + var iconValue = iconProperty.GetValue(skillInfo, null); + if (iconValue is int iconId && iconId > 0) + { + PluginCore.WriteToChat($"Debug: Using direct icon {iconId} from {propName} for skill {skillId}"); + return iconId + 0x6000000; + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: Skill access failed for skill {skillId}: {ex.Message}"); + } + + // Fallback to predefined mapping + PluginCore.WriteToChat($"Debug: Using fallback icon for skill {skillId}"); + return GetFallbackSkillIcon(skillId); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: Error in GetSkillIconId for skill {skillId}: {ex.Message}"); + return GetFallbackSkillIcon(skillId); + } + } + + private string GetSkillName(int skillId) + { + var skillNames = new Dictionary + { + [1] = "Axe", [2] = "Bow", [3] = "Crossbow", [4] = "Dagger", [5] = "Mace", + [6] = "Melee Defense", [7] = "Missile Defense", [8] = "Sling", [9] = "Spear", [10] = "Staff", + [11] = "Sword", [12] = "Thrown Weapons", [13] = "Unarmed Combat", [14] = "Arcane Lore", [15] = "Magic Defense", + [16] = "Mana Conversion", [17] = "Spellcraft", [18] = "Item Tinkering", [19] = "Assess Person", [20] = "Deception", + [21] = "Healing", [22] = "Jump", [23] = "Lockpick", [24] = "Run", [25] = "Awareness", + [26] = "Arms and Armor Repair", [27] = "Assess Creature", [28] = "Weapon Tinkering", [29] = "Armor Tinkering", [30] = "Magic Item Tinkering", + [31] = "Creature Enchantment", [32] = "Item Enchantment", [33] = "Life Magic", [34] = "War Magic", [35] = "Leadership", + [36] = "Loyalty", [37] = "Fletching", [38] = "Alchemy", [39] = "Cooking", [40] = "Salvaging", + [41] = "Two Handed Combat", [42] = "Gearcraft", [43] = "Void Magic", [44] = "Heavy Weapons", [45] = "Light Weapons", + [46] = "Finesse Weapons", [47] = "Missile Weapons", [48] = "Shield", [49] = "Dual Wield", [50] = "Recklessness", + [51] = "Sneak Attack", [52] = "Dirty Fighting", [53] = "Threat Assessment", [54] = "Summoning" + }; + + return skillNames.ContainsKey(skillId) ? skillNames[skillId] : $"Unknown({skillId})"; + } + + private int GetRealSkillIconFromDat(int skillId) + { + try + { + // Try using FileService SkillTable directly (similar to CharacterCreation.cs pattern) + var fileService = CoreManager.Current.Filter(); + if (fileService?.SkillTable != null) + { + // Try to get skill data from the skill table + try + { + // Access SkillTable via reflection to get skill data + var skillTableType = fileService.SkillTable.GetType(); + PluginCore.WriteToChat($"Debug: SkillTable type: {skillTableType.Name}"); + + // Look for methods that can get skill by ID + var methods = skillTableType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var method in methods) + { + if (method.Name.Contains("Get") || method.Name.Contains("get") || method.Name == "Item") + { + var parameters = method.GetParameters(); + if (parameters.Length == 1 && (parameters[0].ParameterType == typeof(int) || parameters[0].ParameterType == typeof(uint))) + { + try + { + var skillData = method.Invoke(fileService.SkillTable, new object[] { skillId }); + if (skillData != null) + { + PluginCore.WriteToChat($"Debug: Found skill data via {method.Name}: {skillData.GetType().Name}"); + + // Look for icon properties on the skill data + var skillDataType = skillData.GetType(); + string[] iconProps = { "IconID", "Icon", "IconId", "GraphicID", "uiGraphic" }; + + foreach (var propName in iconProps) + { + var iconProp = skillDataType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (iconProp != null) + { + var iconValue = iconProp.GetValue(skillData, null); + if (iconValue is int iconInt && iconInt > 0) + { + PluginCore.WriteToChat($"Debug: Found skill icon {iconInt} via FileService.{method.Name}.{propName}"); + return iconInt; + } + else if (iconValue is uint iconUint && iconUint > 0) + { + PluginCore.WriteToChat($"Debug: Found skill icon {iconUint} via FileService.{method.Name}.{propName}"); + return (int)iconUint; + } + } + } + } + } + catch (Exception ex) + { + // Method call failed, try next one + PluginCore.WriteToChat($"Debug: Method {method.Name} failed: {ex.Message}"); + } + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: FileService SkillTable access failed: {ex.Message}"); + } + } + else + { + PluginCore.WriteToChat($"Debug: FileService or SkillTable is null"); + } + + return 0; // No icon found + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: GetRealSkillIconFromDat failed: {ex.Message}"); + return 0; + } + } + + private int? GetFallbackSkillIcon(int skillId) + { + // Use proven working icon IDs from the recall spells system + // These icons are confirmed to display correctly in VVS + var skillIconMap = new Dictionary + { + // Magic Skills - Use magical/mystical icons (from recalls system) + [14] = 0x6002D14, // Arcane Lore - Portal icon (default recall icon) + [16] = 0x60011F9, // Mana Conversion - Green circle (confirmed working) + [31] = 0x6002D14, // Creature Enchantment - Portal icon + [32] = 0x6002D14, // Item Enchantment - Portal icon + [33] = 0x60011F9, // Life Magic - Green circle (life/healing) + [34] = 0x60011F8, // War Magic - Red circle (destruction) + [43] = 0x600287A, // Void Magic - Gray dot (void) + [54] = 0x6002D14, // Summoning - Portal icon + + // Combat Skills - Use distinct working icons + [41] = 0x60011F8, // Two Handed Combat - Red circle + [44] = 0x60011F8, // Heavy Weapons - Red circle + [45] = 0x60028FC, // Light Weapons - Up arrow (from recalls) + [46] = 0x60028FD, // Finesse Weapons - Down arrow (from recalls) + [47] = 0x60020B5, // Missile Weapons - Question mark (from recalls) + [48] = 0x600287A, // Shield - Gray dot + [49] = 0x60011F8, // Dual Wield - Red circle + [50] = 0x60011F8, // Recklessness - Red circle + [51] = 0x600287A, // Sneak Attack - Gray dot + [52] = 0x60011F8, // Dirty Fighting - Red circle + + // Defense Skills - Use defensive icons + [6] = 0x600287A, // Melee Defense - Gray dot + [7] = 0x600287A, // Missile Defense - Gray dot + [15] = 0x600287A, // Magic Defense - Gray dot + + // Misc Skills - Use varied working icons + [19] = 0x60020B5, // Assess Person - Question mark (from recalls) + [20] = 0x60020B5, // Deception - Question mark + [21] = 0x60011F9, // Healing - Green circle + [22] = 0x60028FC, // Jump - Up arrow (motion) + [23] = 0x60020B5, // Lockpick - Question mark + [24] = 0x60028FC, // Run - Up arrow (motion) + [27] = 0x60020B5, // Assess Creature - Question mark + [35] = 0x60020B5, // Leadership - Question mark + [36] = 0x60020B5, // Loyalty - Question mark + + // Craft Skills - Use working craft icons + [18] = 0x600287A, // Item Tinkering - Gray dot + [28] = 0x600287A, // Weapon Tinkering - Gray dot + [29] = 0x600287A, // Armor Tinkering - Gray dot + [30] = 0x600287A, // Magic Item Tinkering - Gray dot + [37] = 0x600287A, // Fletching - Gray dot + [38] = 0x600287A, // Alchemy - Gray dot + [39] = 0x600287A, // Cooking - Gray dot + [40] = 0x600287A, // Salvaging - Gray dot + + // Retired weapon skills - Use weapon-style icons + [1] = 0x60011F8, // Axe - Red circle + [2] = 0x60028FC, // Bow - Up arrow (projectile) + [3] = 0x60028FC, // Crossbow - Up arrow (projectile) + [4] = 0x60011F8, // Dagger - Red circle + [5] = 0x60011F8, // Mace - Red circle + [8] = 0x60028FC, // Sling - Up arrow (projectile) + [9] = 0x60011F8, // Spear - Red circle + [10] = 0x60011F8, // Staff - Red circle + [11] = 0x60011F8, // Sword - Red circle + [12] = 0x60028FC, // Thrown Weapons - Up arrow (projectile) + [13] = 0x60011F8 // Unarmed Combat - Red circle + }; + + if (skillIconMap.ContainsKey(skillId)) + { + PluginCore.WriteToChat($"Debug: Using fallback icon 0x{skillIconMap[skillId]:X} for skill {skillId} ({GetSkillName(skillId)})"); + return skillIconMap[skillId]; + } + + // Final fallback to proven working icon from recalls system + PluginCore.WriteToChat($"Debug: Using default fallback icon 0x6002D14 for unknown skill {skillId}"); + return 0x6002D14; // Portal icon - confirmed working in recalls + } + + private int ComputeCantripIcon(CantripInfo cantrip) + { + try + { + // Green circle for active cantrips, red circle for missing cantrips + if (cantrip.Value != "N/A") + { + return 0x60011F9; // Green circle - has cantrip + } + else + { + return 0x60011F8; // Red circle - missing cantrip + } + } + catch + { + return 0x60011F8; // Red circle on error + } + } + + private void DetectCantrip(int spellId) + { + try + { + // Get spell name from SpellManager + string spellName = GetSpellName(spellId); + if (string.IsNullOrEmpty(spellName)) + { + PluginCore.WriteToChat($"Debug: FAILED to get spell name for spell ID {spellId}"); + return; + } + + // Debug output to see what spells we're processing + PluginCore.WriteToChat($"Debug: Processing spell ID {spellId}: '{spellName}'"); + + // Define cantrip levels and their patterns + var cantripPatterns = new Dictionary + { + ["Minor"] = ("Minor", System.Drawing.Color.White), + ["Moderate"] = ("Moderate", System.Drawing.Color.Green), + ["Major"] = ("Major", System.Drawing.Color.Blue), + ["Epic"] = ("Epic", System.Drawing.Color.Purple), + ["Legendary"] = ("Legendary", System.Drawing.Color.Orange) + }; + + // Check each cantrip level + foreach (var cantripPattern in cantripPatterns) + { + string pattern = cantripPattern.Key; + var (level, color) = cantripPattern.Value; + + if (!spellName.StartsWith(pattern + " ")) continue; + + // Remove the level prefix to get the skill/attribute name + string skillPart = spellName.Substring(pattern.Length + 1); + PluginCore.WriteToChat($"Debug: Found {level} cantrip, skillPart='{skillPart}'"); + + // Get the spell icon for this cantrip spell + int spellIconId = GetRealSpellIcon(spellId); + if (spellIconId == 0) + { + spellIconId = 0x6002D14; // Default fallback icon + PluginCore.WriteToChat($"Debug: No real icon found for spell {spellId}, using default"); + } + else + { + PluginCore.WriteToChat($"Debug: Got spell icon 0x{spellIconId:X} for spell {spellId}"); + } + + // Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward") + if (MatchProtectionAura(skillPart, level, color, spellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched as Protection Aura: '{skillPart}' with spell icon {spellIconId}"); + return; + } + + // Try to match Attributes (exact format: "Minor Strength", "Epic Focus") + if (MatchAttribute(skillPart, level, color, spellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched as Attribute: '{skillPart}' with spell icon {spellIconId}"); + return; + } + + // Try to match Skills using the replacement mappings + if (MatchSkill(skillPart, level, color, spellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched as Skill: '{skillPart}' with spell icon {spellIconId}"); + return; + } + + PluginCore.WriteToChat($"Debug: No match found for: '{skillPart}' (level: {level}) - Full spell: '{spellName}'"); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error detecting cantrip for spell {spellId}: {ex.Message}"); + } + } + + private bool MatchProtectionAura(string skillPart, string level, System.Drawing.Color color, int spellIconId) + { + // Map AC cantrip spell names to protection aura names + var protectionMappings = new Dictionary + { + ["Armor"] = "Armor", + ["Bludgeoning Ward"] = "Bludgeoning Ward", + ["Piercing Ward"] = "Piercing Ward", + ["Slashing Ward"] = "Slashing Ward", + ["Flame Ward"] = "Flame Ward", + ["Frost Ward"] = "Frost Ward", + ["Cold Ward"] = "Frost Ward", // AC also uses "Cold Ward" + ["Acid Ward"] = "Acid Ward", + ["Storm Ward"] = "Storm Ward", // AC spell is "Storm Ward" + ["Lightning Ward"] = "Storm Ward", // AC also uses "Lightning Ward" + + // Add more variations that might appear in AC spell names + ["Bludgeoning Protection"] = "Bludgeoning Ward", + ["Piercing Protection"] = "Piercing Ward", + ["Slashing Protection"] = "Slashing Ward", + ["Fire Protection"] = "Flame Ward", + ["Cold Protection"] = "Frost Ward", + ["Acid Protection"] = "Acid Ward", + ["Lightning Protection"] = "Storm Ward", + + // Single word variations + ["Bludgeoning"] = "Bludgeoning Ward", + ["Piercing"] = "Piercing Ward", + ["Slashing"] = "Slashing Ward", + ["Fire"] = "Flame Ward", + ["Flame"] = "Flame Ward", + ["Cold"] = "Frost Ward", + ["Frost"] = "Frost Ward", + ["Acid"] = "Acid Ward", + ["Lightning"] = "Storm Ward", + ["Storm"] = "Storm Ward" + }; + + foreach (var mapping in protectionMappings) + { + if (skillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) + { + // Create the cantrip entry if it doesn't exist + if (!Cantrips["Protection Auras"].ContainsKey(mapping.Value)) + { + Cantrips["Protection Auras"][mapping.Value] = new CantripInfo + { + Name = mapping.Value, + Value = "N/A", + Color = System.Drawing.Color.White + }; + } + + var cantrip = Cantrips["Protection Auras"][mapping.Value]; + if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) + { + cantrip.Value = level; + cantrip.Color = color; + cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip + } + return true; + } + } + return false; + } + + private bool MatchAttribute(string skillPart, string level, System.Drawing.Color color, int spellIconId) + { + // Clean the skill part - remove extra spaces and normalize + string cleanedSkillPart = skillPart.Trim(); + + var attributeMappings = new Dictionary + { + ["Strength"] = "Strength", + ["Endurance"] = "Endurance", + ["Coordination"] = "Coordination", + ["Quickness"] = "Quickness", + ["Focus"] = "Focus", + ["Self"] = "Willpower", // "Minor Self" -> Willpower + ["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower + }; + + PluginCore.WriteToChat($"Debug: MatchAttribute checking '{cleanedSkillPart}' for {level}"); + + foreach (var mapping in attributeMappings) + { + if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) + { + PluginCore.WriteToChat($"Debug: Found mapping match! '{mapping.Key}' -> '{mapping.Value}'"); + + // Create the cantrip entry if it doesn't exist + if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) + { + Cantrips["Attributes"][mapping.Value] = new CantripInfo + { + Name = mapping.Value, + Value = "N/A", + Color = System.Drawing.Color.White + }; + } + + var cantrip = Cantrips["Attributes"][mapping.Value]; + if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) + { + PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level}"); + cantrip.Value = level; + cantrip.Color = color; + cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip + } + return true; + } + } + + // Try more flexible matching - check if the cleaned skill part contains any of our attributes + foreach (var mapping in attributeMappings) + { + if (cleanedSkillPart.IndexOf(mapping.Key, StringComparison.OrdinalIgnoreCase) >= 0) + { + PluginCore.WriteToChat($"Debug: Found partial mapping match! '{cleanedSkillPart}' contains '{mapping.Key}' -> '{mapping.Value}'"); + + // Create the cantrip entry if it doesn't exist + if (!Cantrips["Attributes"].ContainsKey(mapping.Value)) + { + Cantrips["Attributes"][mapping.Value] = new CantripInfo + { + Name = mapping.Value, + Value = "N/A", + Color = System.Drawing.Color.White + }; + } + + var cantrip = Cantrips["Attributes"][mapping.Value]; + if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) + { + PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level} via partial match"); + cantrip.Value = level; + cantrip.Color = color; + cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip + } + return true; + } + } + + return false; + } + + private bool MatchSkill(string skillPart, string level, System.Drawing.Color color, int spellIconId) + { + // Map actual cantrip spell names to our skill names + var skillMappings = new Dictionary + { + // Defense skills (with AC spell name mappings) + ["Magic Resistance"] = "MagicResistance", + ["Invulnerability"] = "Invulnerability", + ["Impregnability"] = "Impgrenability", // Note: AC uses "Impregnability" not "Impgrenability" + + // Weapon skills (with AC spell name patterns) + ["Heavy Weapon Aptitude"] = "HeavyWeapon", + ["Light Weapon Aptitude"] = "LightWeapon", + ["Finesse Weapon Aptitude"] = "FinesseWeapon", + ["Missile Weapon Aptitude"] = "MissileWeapon", + + // Craft skills (with AC spell name patterns) + ["Alchemical Prowess"] = "Alchemy", + ["Arcane Prowess"] = "Arcane Lore", + ["Armor Tinkering Expertise"] = "Armor Tinkering", + ["Assess Creature"] = "Assess Creature", + ["Assess Person"] = "Assess Person", + ["Cooking Prowess"] = "Cooking", + ["Deception Prowess"] = "Deception", + ["Fletching Prowess"] = "Fletching", + ["Healing Prowess"] = "Healing", + ["Item Tinkering Expertise"] = "Item Tinkering", + ["Leadership"] = "Leadership", + ["Lockpick Prowess"] = "Lockpick", + ["Fealty"] = "Loyalty", // AC uses "Fealty" for Loyalty + ["Magic Item Tinkering Expertise"] = "Magic Item Tinkering", + ["Mana Conversion Prowess"] = "Mana Conversion", + ["Jumping Prowess"] = "Jump", // AC has Jump cantrips + ["Salvaging Aptitude"] = "Salvaging", + ["Weapon Tinkering Expertise"] = "Weapon Tinkering", + + // Magic schools (with AC spell name patterns) + ["War Magic Aptitude"] = "War Magic", + ["Life Magic Aptitude"] = "Life Magic", + ["Creature Enchantment Aptitude"] = "Creature Enchantment", + ["Item Enchantment Aptitude"] = "Item Enchantment", + ["Void Magic Aptitude"] = "Void Magic", + ["Summoning Prowess"] = "Summoning", + + // Combat skills + ["Two Handed Combat Aptitude"] = "Two Handed Combat", + ["Dual Wield Aptitude"] = "Dual Wield", + ["Shield Aptitude"] = "Shield", + ["Sneak Attack Prowess"] = "Sneak Attack", + ["Dirty Fighting Prowess"] = "Dirty Fighting", + ["Recklessness"] = "Recklessness" + }; + + foreach (var mapping in skillMappings) + { + if (skillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) + { + // Check both specialized and trained skills + foreach (var category in new[] { "Specialized Skills", "Trained Skills" }) + { + if (Cantrips[category].ContainsKey(mapping.Value)) + { + var cantrip = Cantrips[category][mapping.Value]; + if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value)) + { + cantrip.Value = level; + cantrip.Color = color; + cantrip.SpellIconId = spellIconId; // Use the actual spell icon instead of skill icon + cantrip.IconId = null; // Clear any skill icon reference + } + return true; + } + } + } + } + return false; + } + + private string GetSpellName(int spellId) + { + try + { + // Use our existing SpellManager that was already working + var spell = SpellManager.GetSpell(spellId); + if (spell != null) + { + return spell.Name; + } + return ""; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Debug: Error getting spell {spellId}: {ex.Message}"); + return ""; + } + } + + private bool IsHigherCantripLevel(string newLevel, string currentLevel) + { + var levels = new Dictionary + { + ["Minor"] = 1, + ["Moderate"] = 2, + ["Major"] = 3, + ["Epic"] = 4, + ["Legendary"] = 5 + }; + + if (!levels.ContainsKey(newLevel) || !levels.ContainsKey(currentLevel)) + return false; + + return levels[newLevel] > levels[currentLevel]; + } + + private void TestCantripDetection() + { + try + { + // Test real AC cantrip spell names to verify the detection logic + var testSpells = new[] + { + "Major Strength", // Attribute cantrip + "Epic Coordination", // Attribute cantrip + "Legendary Focus", // Attribute cantrip + "Major Willpower", // Willpower attribute + "Major Invulnerability", // Melee Defense skill + "Major Impregnability", // Missile Defense skill + "Major Alchemical Prowess", // Alchemy skill + "Major Arcane Prowess", // Arcane Lore skill + "Epic Life Magic Aptitude", // Life Magic skill + "Major Fealty", // Loyalty skill + "Major Armor", // Protection aura + "Epic Flame Ward", // Protection aura + "Legendary Storm Ward" // Protection aura + }; + + foreach (var spellName in testSpells) + { + PluginCore.WriteToChat($"Debug: Testing detection for: {spellName}"); + TestDetectCantripByName(spellName); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error in test cantrip detection: {ex.Message}"); + } + } + + private void TestDetectCantripByName(string spellName) + { + try + { + // Simulate the detection logic with a fake spell name + PluginCore.WriteToChat($"Debug: Processing spell: {spellName}"); + + // Define cantrip levels and their patterns + var cantripPatterns = new Dictionary + { + ["Minor"] = ("Minor", System.Drawing.Color.White), + ["Moderate"] = ("Moderate", System.Drawing.Color.Green), + ["Major"] = ("Major", System.Drawing.Color.Blue), + ["Epic"] = ("Epic", System.Drawing.Color.Purple), + ["Legendary"] = ("Legendary", System.Drawing.Color.Orange) + }; + + // Check each cantrip level + foreach (var cantripPattern in cantripPatterns) + { + string pattern = cantripPattern.Key; + var (level, color) = cantripPattern.Value; + + if (!spellName.StartsWith(pattern + " ")) continue; + + // Remove the level prefix to get the skill/attribute name + string skillPart = spellName.Substring(pattern.Length + 1); + + PluginCore.WriteToChat($"Debug: Detected {level} cantrip for {skillPart}"); + + // Get a test spell icon (use default for testing) + int testSpellIconId = 0x6002D14; + + // Try to match Protection Auras first + if (MatchProtectionAura(skillPart, level, color, testSpellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched protection aura: {skillPart}"); + return; + } + + // Try to match Attributes + if (MatchAttribute(skillPart, level, color, testSpellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched attribute: {skillPart}"); + return; + } + + // Try to match Skills + if (MatchSkill(skillPart, level, color, testSpellIconId)) + { + PluginCore.WriteToChat($"Debug: Matched skill: {skillPart}"); + return; + } + + PluginCore.WriteToChat($"Debug: No match found for: {skillPart}"); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error testing cantrip detection: {ex.Message}"); + } + } + + #region Weapon Data Structures + public class TrackedWeaponInfo + { + public string Name { get; set; } + public string Category { get; set; } + public string WeaponType { get; set; } + public string Status { get; set; } + public bool IsAcquired { get; set; } + public int ItemId { get; set; } + public int IconId { get; set; } + } + + public Dictionary> WeaponCategories { get; private set; } + #endregion + + public void RefreshWeapons() + { + try + { + if (CoreManager.Current?.CharacterFilter?.Name == null) return; + + InitializeNewWeaponData(); + CheckAcquiredWeapons(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing weapons: {ex.Message}"); + } + } + + private void InitializeNewWeaponData() + { + WeaponCategories = new Dictionary> + { + ["Legendary Weapons"] = new List + { + new TrackedWeaponInfo { Name = "Bow of the Quiddity", Category = "Legendary Weapons", WeaponType = "Bow", ItemId = 23044, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Sword of the Quiddity", Category = "Legendary Weapons", WeaponType = "Sword", ItemId = 23039, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Mace of the Quiddity", Category = "Legendary Weapons", WeaponType = "Mace", ItemId = 23040, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlatl of the Quiddity", Category = "Legendary Weapons", WeaponType = "Atlatl", ItemId = 23043, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Staff of the Quiddity", Category = "Legendary Weapons", WeaponType = "Staff", ItemId = 23041, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Dagger of the Quiddity", Category = "Legendary Weapons", WeaponType = "Dagger", ItemId = 23042, Status = "Unknown" } + }, + ["Slayer Weapons"] = new List + { + new TrackedWeaponInfo { Name = "Shadowfire Isparian Bow", Category = "Slayer Weapons", WeaponType = "Bow", ItemId = 20636, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Shadowfire Isparian Sword", Category = "Slayer Weapons", WeaponType = "Sword", ItemId = 20631, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Shadowfire Isparian Staff", Category = "Slayer Weapons", WeaponType = "Staff", ItemId = 20633, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Shadowfire Isparian Dagger", Category = "Slayer Weapons", WeaponType = "Dagger", ItemId = 20634, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Shadowfire Isparian Axe", Category = "Slayer Weapons", WeaponType = "Axe", ItemId = 20635, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Shadowfire Isparian Mace", Category = "Slayer Weapons", WeaponType = "Mace", ItemId = 20632, Status = "Unknown" } + }, + ["Society Weapons"] = new List + { + new TrackedWeaponInfo { Name = "Radiant Blood Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32834, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Celestial Hand Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32835, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Eldritch Web Sword", Category = "Society Weapons", WeaponType = "Sword", ItemId = 32836, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Radiant Blood Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32837, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Celestial Hand Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32838, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Eldritch Web Bow", Category = "Society Weapons", WeaponType = "Bow", ItemId = 32839, Status = "Unknown" } + }, + ["Atlan Weapons"] = new List + { + new TrackedWeaponInfo { Name = "Atlan Sword", Category = "Atlan Weapons", WeaponType = "Sword", ItemId = 11648, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlan Axe", Category = "Atlan Weapons", WeaponType = "Axe", ItemId = 11649, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlan Mace", Category = "Atlan Weapons", WeaponType = "Mace", ItemId = 11650, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlan Spear", Category = "Atlan Weapons", WeaponType = "Spear", ItemId = 11651, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlan Staff", Category = "Atlan Weapons", WeaponType = "Staff", ItemId = 11652, Status = "Unknown" }, + new TrackedWeaponInfo { Name = "Atlan Dagger", Category = "Atlan Weapons", WeaponType = "Dagger", ItemId = 11653, Status = "Unknown" } + } + }; + } + + private void CheckAcquiredWeapons() + { + try + { + // Get inventory items + var worldFilter = CoreManager.Current.WorldFilter; + var inventoryItems = worldFilter.GetInventory().Cast().ToList(); + + // Check each weapon category + foreach (var category in WeaponCategories) + { + foreach (var weapon in category.Value) + { + // Check if weapon is acquired by name matching + var foundItem = inventoryItems.FirstOrDefault(item => + item.Name.Contains(weapon.Name) || + weapon.Name.Contains(item.Name)); + + if (foundItem != null) + { + weapon.IsAcquired = true; + weapon.Status = "Acquired"; + weapon.IconId = foundItem.Icon + 0x6000000; + } + else + { + weapon.IsAcquired = false; + weapon.Status = "Not Acquired"; + weapon.IconId = 0x6002D14; // Default icon + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error checking acquired weapons: {ex.Message}"); + } + } + #endregion + + #region Cleanup + public void Dispose() + { + // Cleanup if needed + } + #endregion + } +} \ No newline at end of file diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index e4c90ea..81236e7 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -104,6 +104,9 @@ Shared\Constants\Dictionaries.cs + + Shared\Spells\Spell.cs + Shared\Constants\DoubleValueKey.cs @@ -160,9 +163,11 @@ + + @@ -179,6 +184,8 @@ True Resources.resx + + @@ -190,8 +197,12 @@ + + + Shared\Spells\Spells.csv + diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 7f1235b..b3f6f4e 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -96,15 +96,15 @@ namespace MosswartMassacre CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete; CoreManager.Current.WorldFilter.CreateObject += OnSpawn; CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn; - + // Initialize VVS view after character login + ViewManager.ViewInit(); // Initialize the timer updateTimer = new Timer(1000); // Update every second updateTimer.Elapsed += UpdateStats; updateTimer.Start(); - // Initialize the view (UI) - use tabbed interface by default - ViewManager.ViewInit(); + // Note: View initialization moved to LoginComplete for VVS compatibility // Enable TLS1.2 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; @@ -187,6 +187,8 @@ namespace MosswartMassacre WriteToChat("Mosswart Massacre has started!"); + + PluginSettings.Initialize(); // Safe to call now // Apply the values @@ -605,7 +607,8 @@ namespace MosswartMassacre 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"); - WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)"); + WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)"); + WriteToChat("/mm gui - Manually initialize/reinitialize GUI"); break; case "report": TimeSpan elapsed = DateTime.Now - statsStartTime; @@ -797,6 +800,20 @@ namespace MosswartMassacre } break; + case "initgui": + case "gui": + try + { + WriteToChat("Attempting to manually initialize GUI..."); + ViewManager.ViewDestroy(); // Clean up any existing view + ViewManager.ViewInit(); // Reinitialize + WriteToChat("GUI initialization attempt completed."); + } + catch (Exception ex) + { + WriteToChat($"GUI initialization error: {ex.Message}"); + } + break; default: WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help"); diff --git a/MosswartMassacre/Properties/AssemblyInfo.cs b/MosswartMassacre/Properties/AssemblyInfo.cs index b034aba..e7d70a2 100644 --- a/MosswartMassacre/Properties/AssemblyInfo.cs +++ b/MosswartMassacre/Properties/AssemblyInfo.cs @@ -26,5 +26,5 @@ using System.Runtime.InteropServices; // Minor Version // Build Number // Revision -[assembly: AssemblyVersion("3.0.1.1")] -[assembly: AssemblyFileVersion("3.0.1.1")] \ No newline at end of file +[assembly: AssemblyVersion("3.0.1.2")] +[assembly: AssemblyFileVersion("3.0.1.2")] \ No newline at end of file diff --git a/MosswartMassacre/QuestManager.cs b/MosswartMassacre/QuestManager.cs new file mode 100644 index 0000000..5973d70 --- /dev/null +++ b/MosswartMassacre/QuestManager.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Decal.Adapter; +using Decal.Adapter.Wrappers; + +namespace MosswartMassacre +{ + /// + /// Quest tracking and management system + /// Ported from UBS Lua quest system + /// + public class QuestManager : IDisposable + { + #region Quest Data Structures + public class Quest + { + public string Id { get; set; } + public int Solves { get; set; } + public int Timestamp { get; set; } + public string Description { get; set; } + public int MaxSolves { get; set; } + public int Delta { get; set; } + public int ExpireTime { get; set; } + } + #endregion + + #region Properties + public List QuestList { get; private set; } + public Dictionary QuestDictionary { get; private set; } + #endregion + + #region Events and State + private bool isRefreshing = false; + private DateTime lastRefreshTime = DateTime.MinValue; + #endregion + + public QuestManager() + { + QuestList = new List(); + QuestDictionary = new Dictionary(); + + // Hook into chat events for quest parsing + InitializeChatHooks(); + } + + #region Initialization + private void InitializeChatHooks() + { + try + { + if (CoreManager.Current != null) + { + CoreManager.Current.ChatBoxMessage += OnChatBoxMessage; + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error initializing quest chat hooks: {ex.Message}"); + } + } + #endregion + + #region Quest Parsing + private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e) + { + try + { + if (!isRefreshing || string.IsNullOrEmpty(e.Text)) + return; + + // Parse quest information from /myquests output + ParseQuestLine(e.Text); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error parsing quest line: {ex.Message}"); + } + } + + private void ParseQuestLine(string text) + { + try + { + // Quest line format: TaskName - Solves solves (Timestamp)"Description" MaxSolves Delta + // Example: "SomeQuest - 5 solves (1640995200)"Quest description here" 10 3600 + var pattern = @"([^\-]+) - (\d+) solves \((\d+)\)""([^""]+)"" (-?\d+) (\d+)"; + var match = Regex.Match(text, pattern); + + if (match.Success) + { + var quest = new Quest + { + Id = match.Groups[1].Value.Trim(), + Solves = int.Parse(match.Groups[2].Value), + Timestamp = int.Parse(match.Groups[3].Value), + Description = match.Groups[4].Value, + MaxSolves = int.Parse(match.Groups[5].Value), + Delta = int.Parse(match.Groups[6].Value) + }; + + quest.ExpireTime = quest.Timestamp + quest.Delta; + + // Add to collections + QuestList.Add(quest); + QuestDictionary[quest.Id] = quest; + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error parsing quest line '{text}': {ex.Message}"); + } + } + #endregion + + #region Quest Management + public void RefreshQuests() + { + try + { + if (isRefreshing) + return; + + ClearQuests(); + isRefreshing = true; + + // Issue /myquests command to refresh quest data + CoreManager.Current.Actions.InvokeChatParser("/myquests"); + + // Stop listening after a delay + System.Threading.Timer stopTimer = null; + stopTimer = new System.Threading.Timer(_ => + { + isRefreshing = false; + stopTimer?.Dispose(); + lastRefreshTime = DateTime.Now; + }, null, 3000, System.Threading.Timeout.Infinite); + } + catch (Exception ex) + { + isRefreshing = false; + PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}"); + } + } + + public void ClearQuests() + { + QuestList.Clear(); + QuestDictionary.Clear(); + } + + public bool IsQuestAvailable(string questStamp) + { + if (!QuestDictionary.TryGetValue(questStamp, out Quest quest)) + return true; // If quest not found, assume available + + var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + return quest.ExpireTime < currentTime; + } + + public bool IsQuestMaxSolved(string questStamp) + { + if (!QuestDictionary.TryGetValue(questStamp, out Quest quest)) + return false; + + return quest.Solves >= quest.MaxSolves; + } + + public bool HasQuestFlag(string questStamp) + { + return QuestDictionary.ContainsKey(questStamp); + } + + public string GetTimeUntilExpire(Quest quest) + { + if (quest == null) + return "Unknown"; + + var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var timeLeft = quest.ExpireTime - currentTime; + + if (timeLeft <= 0) + return "Ready"; + + return FormatSeconds((int)timeLeft); + } + + public string FormatTimeStamp(int timestamp) + { + try + { + var dateTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + return dateTime.ToString("MM/dd/yyyy HH:mm:ss"); + } + catch + { + return "Invalid"; + } + } + + public string FormatSeconds(int seconds) + { + if (seconds <= 0) + return "0s"; + + var days = seconds / 86400; + seconds %= 86400; + var hours = seconds / 3600; + seconds %= 3600; + var minutes = seconds / 60; + seconds %= 60; + + var result = ""; + if (days > 0) result += $"{days}d "; + if (hours > 0) result += $"{hours}h "; + if (minutes > 0) result += $"{minutes}m "; + if (seconds > 0 || string.IsNullOrEmpty(result)) result += $"{seconds}s"; + + return result.Trim(); + } + + public object GetFieldByID(Quest quest, int id) + { + if (quest == null) + return null; + + switch (id) + { + case 1: return quest.Id; + case 2: return quest.Solves; + case 3: return quest.Timestamp; + case 4: return quest.MaxSolves; + case 5: return quest.Delta; + case 6: return quest.ExpireTime; + default: return quest.Id; + } + } + #endregion + + #region Society Quest Helpers + public string GetSocietyName(int factionBits) + { + switch (factionBits) + { + case 1: return "Celestial Hand"; + case 2: return "Eldrytch Web"; + case 4: return "Radiant Blood"; + default: return "Unknown"; + } + } + + public string GetSocietyRank(int ribbons) + { + if (ribbons >= 1001) return "Master"; + if (ribbons >= 601) return "Lord"; + if (ribbons >= 301) return "Knight"; + if (ribbons >= 101) return "Adept"; + if (ribbons >= 1) return "Initiate"; + return "None"; + } + + public int GetMaxRibbonsPerDay(string rank) + { + switch (rank) + { + case "Initiate": return 50; + case "Adept": return 100; + case "Knight": return 150; + case "Lord": return 200; + case "Master": return 250; + default: return 0; + } + } + #endregion + + #region Cleanup + public void Dispose() + { + try + { + if (CoreManager.Current != null) + { + CoreManager.Current.ChatBoxMessage -= OnChatBoxMessage; + } + + ClearQuests(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error disposing quest manager: {ex.Message}"); + } + } + #endregion + } +} \ No newline at end of file diff --git a/MosswartMassacre/SpellManager.cs b/MosswartMassacre/SpellManager.cs new file mode 100644 index 0000000..5d3732b --- /dev/null +++ b/MosswartMassacre/SpellManager.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mag.Shared.Spells; + +namespace MosswartMassacre +{ + /// + /// Manages spell identification and cantrip detection for the Flag Tracker + /// + public static class SpellManager + { + private static readonly Dictionary SpellsById = new Dictionary(); + private static readonly List SpellData = new List(); + private static bool isInitialized = false; + + static SpellManager() + { + Initialize(); + } + + private static void Initialize() + { + if (isInitialized) return; + + try + { + // Load spell data from embedded CSV resource + var assembly = Assembly.GetExecutingAssembly(); + + // Try to find the resource with different naming patterns + var availableResources = assembly.GetManifestResourceNames(); + var spellResource = availableResources.FirstOrDefault(r => r.Contains("Spells.csv")); + + if (string.IsNullOrEmpty(spellResource)) + { + // If not embedded, try to load from file system + var csvPath = Path.Combine(Path.GetDirectoryName(assembly.Location), "..", "Shared", "Spells", "Spells.csv"); + if (File.Exists(csvPath)) + { + LoadFromFile(csvPath); + isInitialized = true; + return; + } + } + else + { + using (var stream = assembly.GetManifestResourceStream(spellResource)) + { + if (stream != null) + { + using (var reader = new StreamReader(stream)) + { + LoadFromReader(reader); + } + } + } + } + + isInitialized = true; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"SpellManager initialization error: {ex.Message}"); + } + } + + private static void LoadFromFile(string path) + { + using (var reader = new StreamReader(path)) + { + LoadFromReader(reader); + } + } + + private static void LoadFromReader(StreamReader reader) + { + // Skip header line + var header = reader.ReadLine(); + + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (!string.IsNullOrWhiteSpace(line)) + { + var parts = line.Split(','); + if (parts.Length >= 6) // Minimum required fields + { + SpellData.Add(parts); + + // Parse spell data + if (int.TryParse(parts[0], out int id)) + { + var name = parts[1]; + int.TryParse(parts[3], out int difficulty); + int.TryParse(parts[4], out int duration); + int.TryParse(parts[5], out int family); + + var spell = new Spell(id, name, difficulty, duration, family); + SpellsById[id] = spell; + } + } + } + } + } + + /// + /// Gets a spell by its ID + /// + public static Spell GetSpell(int id) + { + if (SpellsById.TryGetValue(id, out var spell)) + return spell; + return null; + } + + /// + /// Gets a spell by its name (case-insensitive) + /// + public static Spell GetSpell(string name) + { + return SpellsById.Values.FirstOrDefault(s => + string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets the total number of spells loaded + /// + public static int GetSpellCount() + { + return SpellsById.Count; + } + + /// + /// Detects if a spell is a cantrip and returns its info + /// + public static CantripInfo DetectCantrip(Spell spell) + { + if (spell == null || spell.CantripLevel == Spell.CantripLevels.None) + return null; + + var info = new CantripInfo + { + SpellId = spell.Id, + Name = spell.Name, + Level = GetCantripLevelName(spell.CantripLevel), + Color = GetCantripColor(spell.CantripLevel) + }; + + // Extract skill/attribute name from spell name + info.SkillName = ExtractSkillFromSpellName(spell.Name, info.Level); + + return info; + } + + private static string GetCantripLevelName(Spell.CantripLevels level) + { + switch (level) + { + case Spell.CantripLevels.Minor: return "Minor"; + case Spell.CantripLevels.Moderate: return "Moderate"; + case Spell.CantripLevels.Major: return "Major"; + case Spell.CantripLevels.Epic: return "Epic"; + case Spell.CantripLevels.Legendary: return "Legendary"; + default: return "N/A"; + } + } + + private static System.Drawing.Color GetCantripColor(Spell.CantripLevels level) + { + switch (level) + { + case Spell.CantripLevels.Minor: return System.Drawing.Color.White; + case Spell.CantripLevels.Moderate: return System.Drawing.Color.Green; + case Spell.CantripLevels.Major: return System.Drawing.Color.Blue; + case Spell.CantripLevels.Epic: return System.Drawing.Color.Purple; + case Spell.CantripLevels.Legendary: return System.Drawing.Color.Orange; + default: return System.Drawing.Color.White; + } + } + + private static string ExtractSkillFromSpellName(string spellName, string level) + { + // Remove the cantrip level prefix + var skillPart = spellName; + if (!string.IsNullOrEmpty(level) && skillPart.StartsWith(level + " ")) + { + skillPart = skillPart.Substring(level.Length + 1); + } + + // Map common spell name patterns to skill names + if (skillPart.Contains("Strength")) return "Strength"; + if (skillPart.Contains("Endurance")) return "Endurance"; + if (skillPart.Contains("Coordination")) return "Coordination"; + if (skillPart.Contains("Quickness")) return "Quickness"; + if (skillPart.Contains("Focus")) return "Focus"; + if (skillPart.Contains("Self") || skillPart.Contains("Willpower")) return "Willpower"; + + // Protection mappings + if (skillPart.Contains("Armor")) return "Armor"; + if (skillPart.Contains("Bludgeoning")) return "Bludgeoning Ward"; + if (skillPart.Contains("Piercing")) return "Piercing Ward"; + if (skillPart.Contains("Slashing")) return "Slashing Ward"; + if (skillPart.Contains("Flame") || skillPart.Contains("Fire")) return "Flame Ward"; + if (skillPart.Contains("Frost") || skillPart.Contains("Cold")) return "Frost Ward"; + if (skillPart.Contains("Acid")) return "Acid Ward"; + if (skillPart.Contains("Lightning") || skillPart.Contains("Electric")) return "Storm Ward"; + + // Return the skill part as-is if no mapping found + return skillPart; + } + + /// + /// Information about a detected cantrip + /// + public class CantripInfo + { + public int SpellId { get; set; } + public string Name { get; set; } + public string SkillName { get; set; } + public string Level { get; set; } + public System.Drawing.Color Color { get; set; } + } + } +} \ No newline at end of file diff --git a/MosswartMassacre/Views/FlagTrackerView.cs b/MosswartMassacre/Views/FlagTrackerView.cs new file mode 100644 index 0000000..22cd185 --- /dev/null +++ b/MosswartMassacre/Views/FlagTrackerView.cs @@ -0,0 +1,733 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using VirindiViewService.Controls; + +namespace MosswartMassacre.Views +{ + /// + /// Dedicated Flag Tracker window with comprehensive character tracking + /// Ported from UBS Lua flagtracker with full functionality preservation + /// + internal class FlagTrackerView : VVSBaseView + { + private static FlagTrackerView instance; + + #region Tab Control References + private HudTabView mainTabView; + + // Augmentations Tab + private HudList lstAugmentations; + private HudButton btnRefreshAugs; + + // Luminance Tab + private HudList lstLuminanceAuras; + private HudButton btnRefreshLum; + + // Recalls Tab + private HudList lstRecallSpells; + private HudButton btnRefreshRecalls; + + + // Cantrips Tab + private HudList lstCantrips; + private HudButton btnRefreshCantrips; + + // Weapons Tab + private HudList lstWeapons; + private HudButton btnRefreshWeapons; + + // Quests Tab + private HudList lstQuests; + private HudButton btnRefreshQuests; + + #endregion + + #region Data Management + private FlagTrackerData data; + private QuestManager questManager; + #endregion + + public FlagTrackerView(PluginCore core) : base(core) + { + instance = this; + data = new FlagTrackerData(); + questManager = new QuestManager(); + } + + #region Static Interface + public static void OpenFlagTracker() + { + try + { + if (instance == null) + { + instance = new FlagTrackerView(null); + instance.InitializeView(); + } + else + { + // Bring existing window to front + if (instance.view != null) + { + instance.view.Visible = true; + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}"); + } + } + + public static void CloseFlagTracker() + { + try + { + if (instance != null) + { + instance.Dispose(); + instance = null; + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error closing Flag Tracker: {ex.Message}"); + } + } + + public static bool IsOpen() + { + return instance != null && instance.view != null && instance.view.Visible; + } + #endregion + + #region Initialization + private void InitializeView() + { + try + { + // Create view from XML layout + CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml"); + + // Initialize all tab controls + InitializeTabControls(); + InitializeEventHandlers(); + + // Initialize the base view + Initialize(); + + // Make the view visible + if (view != null) + { + view.Visible = true; + view.ShowInBar = true; + view.Title = "Mossy Tracker v3.0.1.1"; + } + + // Initial data refresh + RefreshAllData(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error initializing Flag Tracker view: {ex.Message}"); + } + } + + private void InitializeTabControls() + { + try + { + // Get main tab view + mainTabView = GetControl("mainTabView"); + + // Augmentations Tab + lstAugmentations = GetControl("lstAugmentations"); + btnRefreshAugs = GetControl("btnRefreshAugs"); + + // Luminance Tab + lstLuminanceAuras = GetControl("lstLuminanceAuras"); + btnRefreshLum = GetControl("btnRefreshLum"); + + // Recalls Tab + lstRecallSpells = GetControl("lstRecallSpells"); + btnRefreshRecalls = GetControl("btnRefreshRecalls"); + + // Cantrips Tab + lstCantrips = GetControl("lstCantrips"); + btnRefreshCantrips = GetControl("btnRefreshCantrips"); + + // Weapons Tab + lstWeapons = GetControl("lstWeapons"); + btnRefreshWeapons = GetControl("btnRefreshWeapons"); + + // Quests Tab + lstQuests = GetControl("lstQuests"); + btnRefreshQuests = GetControl("btnRefreshQuests"); + + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error initializing tab controls: {ex.Message}"); + } + } + + private void InitializeEventHandlers() + { + try + { + // Refresh button events + if (btnRefreshAugs != null) btnRefreshAugs.Hit += OnRefreshAugmentations; + if (btnRefreshLum != null) btnRefreshLum.Hit += OnRefreshLuminance; + if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls; + if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips; + if (btnRefreshWeapons != null) btnRefreshWeapons.Hit += OnRefreshWeapons; + if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests; + + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error initializing event handlers: {ex.Message}"); + } + } + #endregion + + #region Event Handlers + private void OnRefreshAugmentations(object sender, EventArgs e) + { + try + { + data.RefreshAugmentations(); + PopulateAugmentationsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing augmentations: {ex.Message}"); + } + } + + private void OnRefreshLuminance(object sender, EventArgs e) + { + try + { + data.RefreshLuminanceAuras(); + PopulateLuminanceList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing luminance auras: {ex.Message}"); + } + } + + private void OnRefreshRecalls(object sender, EventArgs e) + { + try + { + data.RefreshRecallSpells(); + PopulateRecallsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing recall spells: {ex.Message}"); + } + } + + + private void OnRefreshCantrips(object sender, EventArgs e) + { + try + { + data.RefreshCantrips(); + PopulateCantripsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing cantrips: {ex.Message}"); + } + } + + private void OnRefreshWeapons(object sender, EventArgs e) + { + try + { + data.RefreshWeapons(); + PopulateWeaponsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing weapons: {ex.Message}"); + } + } + + private void OnRefreshQuests(object sender, EventArgs e) + { + try + { + questManager.RefreshQuests(); + PopulateQuestsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}"); + } + } + + #endregion + + #region Helper Methods + private void SafeSetListText(HudList.HudListRowAccessor row, int columnIndex, string text) + { + try + { + if (row != null && columnIndex >= 0) + { + // Check if the column exists + try + { + var control = row[columnIndex]; + if (control != null) + { + ((HudStaticText)control).Text = text ?? ""; + } + } + catch (IndexOutOfRangeException) + { + // Column doesn't exist - ignore silently + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error setting list text at column {columnIndex}: {ex.Message}"); + } + } + + private void SafeSetListColor(HudList.HudListRowAccessor row, int columnIndex, Color color) + { + try + { + if (row != null && columnIndex >= 0 && row[columnIndex] != null) + { + ((HudStaticText)row[columnIndex]).TextColor = color; + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error setting list color at column {columnIndex}: {ex.Message}"); + } + } + + private void SafeSetListImage(HudList.HudListRowAccessor row, int columnIndex, int iconId) + { + try + { + if (row != null && columnIndex >= 0) + { + try + { + var control = row[columnIndex]; + + if (control != null && control is VirindiViewService.Controls.HudPictureBox) + { + var pictureBox = (VirindiViewService.Controls.HudPictureBox)control; + pictureBox.Image = iconId; + } + } + catch (IndexOutOfRangeException) + { + // Column doesn't exist - ignore silently + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error setting list image at column {columnIndex}: {ex.Message}"); + } + } + + private string GetIconSymbol(string category) + { + switch (category) + { + case "Basic Recalls": + return "[B]"; // Basic recalls + case "Island Recalls": + return "[I]"; // Island recalls + case "Town Recalls": + return "[T]"; // Town recalls + case "Special Recalls": + return "[S]"; // Special recalls + default: + return "[?]"; // Unknown category + } + } + #endregion + + #region Data Population Methods + private void RefreshAllData() + { + try + { + questManager.RefreshQuests(); + data.RefreshAll(); + + PopulateAugmentationsList(); + PopulateLuminanceList(); + PopulateRecallsList(); + PopulateCantripsList(); + PopulateQuestsList(); + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error refreshing all data: {ex.Message}"); + } + } + + private void PopulateAugmentationsList() + { + try + { + if (lstAugmentations == null || data?.AugmentationCategories == null) return; + + lstAugmentations.ClearRows(); + + foreach (var category in data.AugmentationCategories) + { + // Add category header + var headerRow = lstAugmentations.AddRow(); + SafeSetListText(headerRow, 0, $"--- {category.Key} ---"); + SafeSetListText(headerRow, 1, ""); + SafeSetListText(headerRow, 2, ""); + SafeSetListText(headerRow, 3, ""); + + // Add augmentations in this category + foreach (var aug in category.Value) + { + var row = lstAugmentations.AddRow(); + + // Augmentation name with progress indicator + string progressText = aug.IsMaxed ? "[MAX]" : $"[{aug.CurrentValue}/{aug.Repeatable}]"; + SafeSetListText(row, 0, aug.Name); + SafeSetListText(row, 1, progressText); + SafeSetListText(row, 2, aug.Trainer); + SafeSetListText(row, 3, aug.Location); + + // Color code based on completion status + Color progressColor = Color.Red; + if (aug.IsMaxed) + { + progressColor = Color.Green; + } + else if (aug.CurrentValue > 0) + { + progressColor = Color.Yellow; + } + + // Apply color to progress text + SafeSetListColor(row, 1, progressColor); + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating augmentations list: {ex.Message}"); + } + } + + private void PopulateLuminanceList() + { + try + { + if (lstLuminanceAuras == null || data?.LuminanceAuraCategories == null) return; + + lstLuminanceAuras.ClearRows(); + + foreach (var category in data.LuminanceAuraCategories) + { + // Add category header + var headerRow = lstLuminanceAuras.AddRow(); + SafeSetListText(headerRow, 0, $"--- {category.Key} ---"); + SafeSetListText(headerRow, 1, ""); + SafeSetListText(headerRow, 2, ""); + + // Add luminance auras in this category + foreach (var aura in category.Value) + { + var row = lstLuminanceAuras.AddRow(); + + // Aura name + SafeSetListText(row, 0, aura.Name); + + // Progress (current/cap) + string progressText = $"{aura.CurrentValue}/{aura.Cap}"; + SafeSetListText(row, 1, progressText); + + // Category or quest flag for Seer auras + string categoryText = category.Key == "Seer Auras" && !string.IsNullOrEmpty(aura.QuestFlag) + ? aura.QuestFlag + : category.Key; + SafeSetListText(row, 2, categoryText); + + // Color code based on progress + Color progressColor = Color.Red; + if (aura.CurrentValue >= aura.Cap) + { + progressColor = Color.Green; + } + else if (aura.CurrentValue > 0) + { + progressColor = Color.Yellow; + } + + // Apply color to progress text + SafeSetListColor(row, 1, progressColor); + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating luminance list: {ex.Message}"); + } + } + + private void PopulateRecallsList() + { + try + { + if (lstRecallSpells == null || data?.RecallSpells == null) return; + + lstRecallSpells.ClearRows(); + + foreach (var recall in data.RecallSpells) + { + var row = lstRecallSpells.AddRow(); + + + // Column 0: Spell icon using MagTools approach + SafeSetListImage(row, 0, recall.IconId); + + // Column 1: Recall spell name + SafeSetListText(row, 1, recall.Name); + + // Column 2: Known status + string status = recall.IsKnown ? "Known" : "Unknown"; + SafeSetListText(row, 2, status); + + // Color code based on known status + Color statusColor = recall.IsKnown ? Color.Green : Color.Red; + SafeSetListColor(row, 2, statusColor); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating recalls list: {ex.Message}"); + } + } + + + private void PopulateCantripsList() + { + try + { + if (lstCantrips == null || data?.Cantrips == null) return; + + lstCantrips.ClearRows(); + + foreach (var category in data.Cantrips) + { + // Add category header + var headerRow = lstCantrips.AddRow(); + SafeSetListImage(headerRow, 0, 0x6002856); // Star icon for category headers + SafeSetListText(headerRow, 1, $"--- {category.Key} ---"); + SafeSetListText(headerRow, 2, ""); + + // Add cantrips in this category + foreach (var cantrip in category.Value) + { + var row = lstCantrips.AddRow(); + + // Column 0: Icon (green/red circle based on status) + SafeSetListImage(row, 0, cantrip.Value.ComputedIconId); + + // Column 1: Skill/Attribute name + SafeSetListText(row, 1, cantrip.Key); + + // Column 2: Cantrip level + SafeSetListText(row, 2, cantrip.Value.Value); + + // Apply color coding based on cantrip level + SafeSetListColor(row, 2, cantrip.Value.Color); + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating cantrips list: {ex.Message}"); + } + } + + private void PopulateWeaponsList() + { + try + { + if (lstWeapons == null || data?.WeaponCategories == null) return; + + lstWeapons.ClearRows(); + + foreach (var category in data.WeaponCategories) + { + // Add category header + var headerRow = lstWeapons.AddRow(); + SafeSetListText(headerRow, 0, $"--- {category.Key} ---"); + SafeSetListText(headerRow, 1, ""); + SafeSetListText(headerRow, 2, ""); + SafeSetListText(headerRow, 3, ""); + + // Add weapons in this category + foreach (var weapon in category.Value) + { + var row = lstWeapons.AddRow(); + + // Column 0: Category + SafeSetListText(row, 0, weapon.Category); + + // Column 1: Weapon Type + SafeSetListText(row, 1, weapon.WeaponType); + + // Column 2: Weapon Name + SafeSetListText(row, 2, weapon.Name); + + // Column 3: Status + SafeSetListText(row, 3, weapon.Status); + + // Color code based on acquisition status + System.Drawing.Color statusColor = weapon.IsAcquired ? + System.Drawing.Color.Green : System.Drawing.Color.Red; + SafeSetListColor(row, 3, statusColor); + } + } + + if (data.WeaponCategories.Count == 0) + { + var row = lstWeapons.AddRow(); + SafeSetListText(row, 0, "No weapon data - click Refresh"); + SafeSetListText(row, 1, ""); + SafeSetListText(row, 2, ""); + SafeSetListText(row, 3, ""); + SafeSetListColor(row, 0, System.Drawing.Color.Gray); + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating weapons list: {ex.Message}"); + } + } + + private void PopulateQuestsList() + { + try + { + if (lstQuests == null) + { + PluginCore.WriteToChat("Quest list control is null"); + return; + } + + lstQuests.ClearRows(); + + // Always show debug info for now + var row = lstQuests.AddRow(); + SafeSetListText(row, 0, $"Quest Manager: {(questManager != null ? "OK" : "NULL")}"); + SafeSetListText(row, 1, $"Quest Count: {questManager?.QuestList?.Count ?? 0}"); + SafeSetListText(row, 2, "Click Refresh to load quest data"); + SafeSetListText(row, 3, ""); + SafeSetListText(row, 4, ""); + SafeSetListText(row, 5, ""); + + if (questManager?.QuestList != null && questManager.QuestList.Count > 0) + { + foreach (var quest in questManager.QuestList.OrderBy(q => q.Id)) + { + var questRow = lstQuests.AddRow(); + + // Column 0: Quest Name + SafeSetListText(questRow, 0, quest.Id); + + // Column 1: Solves + SafeSetListText(questRow, 1, quest.Solves.ToString()); + + // Column 2: Completed date + SafeSetListText(questRow, 2, questManager.FormatTimeStamp(quest.Timestamp)); + + // Column 3: Max solves + string maxText = quest.MaxSolves < 0 ? "∞" : quest.MaxSolves.ToString(); + SafeSetListText(questRow, 3, maxText); + + // Column 4: Delta (cooldown in seconds) + SafeSetListText(questRow, 4, questManager.FormatSeconds(quest.Delta)); + + // Column 5: Expire time + string expireText = questManager.GetTimeUntilExpire(quest); + SafeSetListText(questRow, 5, expireText); + + // Color coding based on availability + var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + if (quest.MaxSolves > 0 && quest.Solves >= quest.MaxSolves) + { + // Quest is maxed out - red + SafeSetListColor(questRow, 1, System.Drawing.Color.Red); + SafeSetListColor(questRow, 5, System.Drawing.Color.Red); + } + else if (quest.ExpireTime <= currentTime) + { + // Quest is available - green + SafeSetListColor(questRow, 5, System.Drawing.Color.Green); + } + else + { + // Quest is on cooldown - yellow + SafeSetListColor(questRow, 5, System.Drawing.Color.Yellow); + } + } + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error populating quests list: {ex.Message}"); + } + } + #endregion + + + #region Cleanup + protected override void Dispose(bool disposing) + { + if (disposing) + { + try + { + if (data != null) + { + data.Dispose(); + data = null; + } + + if (questManager != null) + { + questManager.Dispose(); + questManager = null; + } + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error disposing Flag Tracker: {ex.Message}"); + } + } + + base.Dispose(disposing); + } + #endregion + } +} \ No newline at end of file diff --git a/MosswartMassacre/Views/VVSTabbedMainView.cs b/MosswartMassacre/Views/VVSTabbedMainView.cs index a2c9ad8..9c63728 100644 --- a/MosswartMassacre/Views/VVSTabbedMainView.cs +++ b/MosswartMassacre/Views/VVSTabbedMainView.cs @@ -54,6 +54,11 @@ namespace MosswartMassacre.Views private HudButton btnClearRoute; #endregion + #region Flag Tracker Tab Controls + private HudButton btnOpenFlagTracker; + private HudStaticText lblFlagTrackerStatus; + #endregion + #region Statistics Tracking private double bestHourlyKills = 0; private DateTime sessionStartTime; @@ -70,15 +75,28 @@ namespace MosswartMassacre.Views { try { + // Check if VVS is available + try + { + var testParser = new VirindiViewService.XMLParsers.Decal3XMLParser(); + } + catch (Exception vvsEx) + { + PluginCore.WriteToChat("[ERROR] VVS not available: " + vvsEx.Message); + return; + } + if (instance == null) { instance = new VVSTabbedMainView(null); } + instance.InitializeView(); } catch (Exception ex) { PluginCore.WriteToChat("Error initializing VVS tabbed view: " + ex.Message); + PluginCore.WriteToChat("Stack trace: " + ex.StackTrace); } } @@ -107,11 +125,18 @@ namespace MosswartMassacre.Views // Create view from working original XML layout CreateFromXMLResource("MosswartMassacre.ViewXML.mainViewTabbed.xml"); + if (view == null) + { + PluginCore.WriteToChat("[ERROR] View creation failed - view is null!"); + return; + } + // Initialize all tab controls InitializeMainTabControls(); InitializeSettingsTabControls(); InitializeStatisticsTabControls(); InitializeNavigationTabControls(); + InitializeFlagTrackerTabControls(); // Initialize the base view and set initial position Initialize(); @@ -121,11 +146,13 @@ namespace MosswartMassacre.Views { view.Visible = true; view.ShowInBar = true; + PluginCore.WriteToChat("GUI initialized successfully!"); } } catch (Exception ex) { PluginCore.WriteToChat("Error in VVS InitializeView: " + ex.Message); + PluginCore.WriteToChat("Stack trace: " + ex.StackTrace); } } @@ -246,6 +273,28 @@ namespace MosswartMassacre.Views PluginCore.WriteToChat($"Error initializing navigation controls: {ex.Message}"); } } + + private void InitializeFlagTrackerTabControls() + { + try + { + // Flag Tracker tab controls + btnOpenFlagTracker = GetControl("btnOpenFlagTracker"); + lblFlagTrackerStatus = GetControl("lblFlagTrackerStatus"); + + // Hook up Flag Tracker events + if (btnOpenFlagTracker != null) + btnOpenFlagTracker.Hit += OnOpenFlagTrackerClick; + + // Update initial status + if (lblFlagTrackerStatus != null) + lblFlagTrackerStatus.Text = "Status: Click to open the Flag Tracker window"; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error initializing flag tracker controls: {ex.Message}"); + } + } #endregion #region Event Handlers - Settings Tab @@ -409,6 +458,31 @@ namespace MosswartMassacre.Views } #endregion + #region Event Handlers - Flag Tracker Tab + private void OnOpenFlagTrackerClick(object sender, EventArgs e) + { + try + { + // Update status to show opening + if (lblFlagTrackerStatus != null) + lblFlagTrackerStatus.Text = "Status: Opening Flag Tracker window..."; + + // Open the Flag Tracker window + FlagTrackerView.OpenFlagTracker(); + + // Update status + if (lblFlagTrackerStatus != null) + lblFlagTrackerStatus.Text = "Status: Flag Tracker window is open"; + } + catch (Exception ex) + { + PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}"); + if (lblFlagTrackerStatus != null) + lblFlagTrackerStatus.Text = "Status: Error opening Flag Tracker"; + } + } + #endregion + #region Event Handlers - Navigation Tab private void OnNavVisualizationEnabledChanged(object sender, EventArgs e) {