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)
{