MosswartMassacre/MosswartMassacre/FlagTrackerData.cs
2025-06-22 12:10:15 +02:00

1932 lines
No EOL
99 KiB
C#

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
{
/// <summary>
/// Data management class for Flag Tracker
/// Ported from UBS Lua flagtracker data structures
/// </summary>
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<string, List<AugmentationInfo>> 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<string, List<LuminanceAuraInfo>> 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<RecallSpellInfo> 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<string, List<SocietyQuestInfo>> 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<string, List<CharacterFlagInfo>> 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<string, Dictionary<string, CantripInfo>> Cantrips { get; private set; }
// Skill name mappings for cantrips that have different names than their skills
private readonly Dictionary<int, string> SkillCantripReplacements = new Dictionary<int, string>
{
[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<string, object> cachedValues = new Dictionary<string, object>();
#endregion
#region Initialization
private void InitializeDataStructures()
{
InitializeAugmentationData();
InitializeLuminanceAuraData();
InitializeRecallSpellData();
InitializeSocietyQuestData();
InitializeCharacterFlagData();
InitializeCantripData();
InitializeNewWeaponData();
}
private void InitializeAugmentationData()
{
AugmentationCategories = new Dictionary<string, List<AugmentationInfo>>
{
["Death Augs"] = new List<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<AugmentationInfo>
{
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<string, List<LuminanceAuraInfo>>
{
["Nalicana Auras"] = new List<LuminanceAuraInfo>
{
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<LuminanceAuraInfo>
{
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<RecallSpellInfo>
{
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<string, List<SocietyQuestInfo>>();
// TODO: Initialize society quest data from Lua
}
private void InitializeCharacterFlagData()
{
CharacterFlags = new Dictionary<string, List<CharacterFlagInfo>>
{
["Additional Skill Credits"] = new List<CharacterFlagInfo>
{
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<CharacterFlagInfo>
{
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<string, Dictionary<string, CantripInfo>>
{
["Specialized Skills"] = new Dictionary<string, CantripInfo>(), // Dynamically populated
["Trained Skills"] = new Dictionary<string, CantripInfo>(), // Dynamically populated
["Attributes"] = new Dictionary<string, CantripInfo>(), // Dynamically populated when cantrips are detected
["Protection Auras"] = new Dictionary<string, CantripInfo>
{
// 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);
}
catch
{
// 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 });
}
else
{
aug.CurrentValue = 0;
}
}
catch
{
aug.CurrentValue = 0;
}
}
}
else
{
aug.CurrentValue = 0;
}
}
else
{
aug.CurrentValue = 0;
}
}
catch
{
aug.CurrentValue = 0;
}
}
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);
}
catch
{
aura.CurrentValue = 0;
}
}
}
}
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
{
recall.IsKnown = false;
}
}
}
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<int, int>
{
// 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<Decal.Filters.FileService>();
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
{
}
// Method 2: Use known spell icon mappings for cantrips
// These are based on AC spell icon IDs from spell data
var cantripSpellIcons = new Dictionary<int, int>
{
// 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;
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
{
if (CoreManager.Current?.CharacterFilter?.Name == null)
{
return;
}
var characterFilter = CoreManager.Current.CharacterFilter;
var playerObject = CoreManager.Current.WorldFilter[characterFilter.Id];
if (playerObject == null)
{
return;
}
// 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)
{
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)
{
DetectCantrip(ench.SpellId);
}
}
}
else
{
}
// 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<int, string>
{
[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)
{
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)
{
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)
{
return GetFallbackSkillIcon(skillId);
}
// Validate skillId range for DECAL API
if (skillId < 1 || skillId > 54)
{
return GetFallbackSkillIcon(skillId);
}
try
{
var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId];
if (skillInfo == null)
{
return GetFallbackSkillIcon(skillId);
}
// Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property)
var skillType = skillInfo.GetType();
// Method 1: Try FileService SkillTable approach (most reliable)
int realIconId = GetRealSkillIconFromDat(skillId);
if (realIconId > 0)
{
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();
// 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)
{
if (iconValue is int iconId && iconId > 0)
{
return iconId + 0x6000000;
}
else if (iconValue is uint uiconId && uiconId > 0)
{
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)
{
if (iconValue is int iconId && iconId > 0)
{
return iconId + 0x6000000;
}
else if (iconValue is uint uiconId && uiconId > 0)
{
return (int)uiconId + 0x6000000;
}
}
}
}
}
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}: <error: {ex.Message}> ({prop.PropertyType.Name})");
}
}
}
else
{
}
}
else
{
}
// 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)
{
return iconId + 0x6000000;
}
}
}
}
catch
{
}
// Fallback to predefined mapping
return GetFallbackSkillIcon(skillId);
}
catch
{
return GetFallbackSkillIcon(skillId);
}
}
private string GetSkillName(int skillId)
{
var skillNames = new Dictionary<int, string>
{
[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<Decal.Filters.FileService>();
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();
// 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)
{
// 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)
{
return iconInt;
}
else if (iconValue is uint iconUint && iconUint > 0)
{
return (int)iconUint;
}
}
}
}
}
catch
{
// Method call failed, try next one
}
}
}
}
}
catch
{
}
}
else
{
}
return 0; // No icon found
}
catch
{
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<int, int>
{
// 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))
{
return skillIconMap[skillId];
}
// Final fallback to proven working icon from recalls system
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))
{
return;
}
// Debug output to see what spells we're processing
// Define cantrip levels and their patterns
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
{
["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);
// Get the spell icon for this cantrip spell
int spellIconId = GetRealSpellIcon(spellId);
if (spellIconId == 0)
{
spellIconId = 0x6002D14; // Default fallback icon
}
else
{
}
// Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward")
if (MatchProtectionAura(skillPart, level, color, spellIconId))
{
return;
}
// Try to match Attributes (exact format: "Minor Strength", "Epic Focus")
if (MatchAttribute(skillPart, level, color, spellIconId))
{
return;
}
// Try to match Skills using the replacement mappings
if (MatchSkill(skillPart, level, color, spellIconId))
{
return;
}
}
}
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<string, string>
{
["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<string, string>
{
["Strength"] = "Strength",
["Endurance"] = "Endurance",
["Coordination"] = "Coordination",
["Quickness"] = "Quickness",
["Focus"] = "Focus",
["Self"] = "Willpower", // "Minor Self" -> Willpower
["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower
};
foreach (var mapping in attributeMappings)
{
if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase))
{
// 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))
{
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)
{
// 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))
{
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<string, string>
{
// 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
{
return "";
}
}
private bool IsHigherCantripLevel(string newLevel, string currentLevel)
{
var levels = new Dictionary<string, int>
{
["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)
{
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
// Define cantrip levels and their patterns
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
{
["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);
// Get a test spell icon (use default for testing)
int testSpellIconId = 0x6002D14;
// Try to match Protection Auras first
if (MatchProtectionAura(skillPart, level, color, testSpellIconId))
{
return;
}
// Try to match Attributes
if (MatchAttribute(skillPart, level, color, testSpellIconId))
{
return;
}
// Try to match Skills
if (MatchSkill(skillPart, level, color, testSpellIconId))
{
return;
}
}
}
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<string, List<TrackedWeaponInfo>> 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<string, List<TrackedWeaponInfo>>
{
["Legendary Weapons"] = new List<TrackedWeaponInfo>
{
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<TrackedWeaponInfo>
{
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<TrackedWeaponInfo>
{
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<TrackedWeaponInfo>
{
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<WorldObject>().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
}
}