using System; using System.Collections.Generic; using System.Linq; using ACE.Database; using ACE.Database.Models.Shard; using ACE.DatLoader; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Managers; using ACE.Server.Network.Structure; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { partial class Player { public bool SpellIsKnown(uint spellId) { return Biota.SpellIsKnown((int)spellId, BiotaDatabaseLock); } /// /// Will return true if the spell was added, or false if the spell already exists. /// public bool AddKnownSpell(uint spellId) { Biota.GetOrAddKnownSpell((int)spellId, BiotaDatabaseLock, out var spellAdded); if (spellAdded) ChangesDetected = true; return spellAdded; } /// /// Removes a known spell from the player's spellbook /// public bool RemoveKnownSpell(uint spellId) { return Biota.TryRemoveKnownSpell((int)spellId, BiotaDatabaseLock); } public void LearnSpellWithNetworking(uint spellId, bool uiOutput = true) { var spells = DatManager.PortalDat.SpellTable; if (!spells.Spells.ContainsKey(spellId)) { GameMessageSystemChat errorMessage = new GameMessageSystemChat("SpellID not found in Spell Table", ChatMessageType.Broadcast); Session.Network.EnqueueSend(errorMessage); return; } if (!AddKnownSpell(spellId)) { if (uiOutput) { GameMessageSystemChat errorMessage = new GameMessageSystemChat("You already know that spell!", ChatMessageType.Broadcast); Session.Network.EnqueueSend(errorMessage); } return; } GameEventMagicUpdateSpell updateSpellEvent = new GameEventMagicUpdateSpell(Session, (ushort)spellId); Session.Network.EnqueueSend(updateSpellEvent); // Check to see if we echo output to the client text area and do playscript animation if (uiOutput) { // Always seems to be this SkillUpPurple effect ApplyVisualEffects(PlayScript.SkillUpPurple); string message = $"You learn the {spells.Spells[spellId].Name} spell.\n"; GameMessageSystemChat learnMessage = new GameMessageSystemChat(message, ChatMessageType.Broadcast); Session.Network.EnqueueSend(learnMessage); } else { Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "You have learned a new spell.")); } } /// /// Learns spells in bulk, without notification, filtered by school and level /// public void LearnSpellsInBulk(MagicSchool school, uint spellLevel, bool withNetworking = true) { var spellTable = DatManager.PortalDat.SpellTable; foreach (var spellID in PlayerSpellTable) { if (!spellTable.Spells.ContainsKey(spellID)) { Console.WriteLine($"Unknown spell ID in PlayerSpellID table: {spellID}"); continue; } var spell = new Spell(spellID, false); if (spell.School == school && spell.Formula.Level == spellLevel) { if (withNetworking) LearnSpellWithNetworking(spell.Id, false); else AddKnownSpell(spell.Id); } } } public void HandleActionMagicRemoveSpellId(uint spellId) { if (!Biota.TryRemoveKnownSpell((int)spellId, BiotaDatabaseLock)) { log.Error("Invalid spellId passed to Player.RemoveSpellFromSpellBook"); return; } ChangesDetected = true; GameEventMagicRemoveSpell removeSpellEvent = new GameEventMagicRemoveSpell(Session, (ushort)spellId); Session.Network.EnqueueSend(removeSpellEvent); } public void EquipItemFromSet(WorldObject item) { if (!item.HasItemSet) return; var setItems = EquippedObjects.Values.Where(i => i.HasItemSet && i.EquipmentSetId == item.EquipmentSetId).ToList(); var spells = GetSpellSet(setItems); // get the spells from before / without this item setItems.Remove(item); var prevSpells = GetSpellSet(setItems); EquipDequipItemFromSet(item, spells, prevSpells); } public void EquipDequipItemFromSet(WorldObject item, List spells, List prevSpells, WorldObject surrogateItem = null) { // compare these 2 spell sets - // see which spells are being added, and which are being removed var addSpells = spells.Except(prevSpells); var removeSpells = prevSpells.Except(spells); // set spells are not affected by mana // if it's equipped, it's active. foreach (var spell in removeSpells) EnchantmentManager.Dispel(EnchantmentManager.GetEnchantment(spell.Id, item.EquipmentSetId.Value)); var addItem = surrogateItem ?? item; foreach (var spell in addSpells) CreateItemSpell(addItem, spell.Id); } public void DequipItemFromSet(WorldObject item) { if (!item.HasItemSet) return; var setItems = EquippedObjects.Values.Where(i => i.HasItemSet && i.EquipmentSetId == item.EquipmentSetId).ToList(); // for better bookkeeping, and to avoid a rarish error with AuditItemSpells detecting -1 duration item enchantments where // the CasterGuid is no longer in the player's possession var surrogateItem = setItems.LastOrDefault(); var spells = GetSpellSet(setItems); // get the spells from before / with this item setItems.Add(item); var prevSpells = GetSpellSet(setItems); if (surrogateItem == null) { var addSpells = spells.Except(prevSpells); if (addSpells.Count() != 0) log.Error($"{Name}.DequipItemFromSet({item.Name}) -- last item in set dequipped, but addSpells still contains {string.Join(", ", addSpells.Select(i => i.Name))} -- this shouldn't happen!"); } EquipDequipItemFromSet(item, spells, prevSpells, surrogateItem); } public void OnItemLevelUp(WorldObject item, int prevItemLevel) { if (!item.HasItemSet) return; var setItems = EquippedObjects.Values.Where(i => i.HasItemSet && i.EquipmentSetId == item.EquipmentSetId).ToList(); var levelDiff = prevItemLevel - (item.ItemLevel ?? 0); var prevSpells = GetSpellSet(setItems, levelDiff); var spells = GetSpellSet(setItems); EquipDequipItemFromSet(item, spells, prevSpells); } public void CreateSentinelBuffPlayers(IEnumerable players, bool self = false, ulong maxLevel = 8) { if (!(Session.AccessLevel >= AccessLevel.Sentinel)) return; var SelfOrOther = self ? "Self" : "Other"; // ensure level 8s are installed var maxSpellLevel = Math.Clamp(maxLevel, 1, 8); if (maxSpellLevel == 8 && DatabaseManager.World.GetCachedSpell((uint)SpellId.ArmorOther8) == null) maxSpellLevel = 7; var tySpell = typeof(SpellId); List buffMessages = new List(); // prepare messages List buffsNotImplementedYet = new List(); foreach (var spell in Buffs) { var spellNamPrefix = spell; bool isBane = false; if (spellNamPrefix.StartsWith("@")) { isBane = true; spellNamPrefix = spellNamPrefix.Substring(1); } string fullSpellEnumName = spellNamPrefix + ((isBane) ? string.Empty : SelfOrOther) + maxSpellLevel; string fullSpellEnumNameAlt = spellNamPrefix + ((isBane) ? string.Empty : ((SelfOrOther == "Self") ? "Other" : "Self")) + maxSpellLevel; uint spellID = (uint)Enum.Parse(tySpell, fullSpellEnumName); var buffMsg = BuildBuffMessage(spellID); if (buffMsg == null) { spellID = (uint)Enum.Parse(tySpell, fullSpellEnumNameAlt); buffMsg = BuildBuffMessage(spellID); } if (buffMsg != null) { buffMsg.Bane = isBane; buffMessages.Add(buffMsg); } else { buffsNotImplementedYet.Add(fullSpellEnumName); } } // buff each player players.ToList().ForEach(targetPlayer => { if (buffMessages.Any(k => !k.Bane)) { // bake player into the messages buffMessages.Where(k => !k.Bane).ToList().ForEach(k => k.SetTargetPlayer(targetPlayer)); // update client-side enchantments targetPlayer.Session.Network.EnqueueSend(buffMessages.Where(k => !k.Bane).Select(k => k.SessionMessage).ToArray()); // run client-side effect scripts, omitting duplicates targetPlayer.EnqueueBroadcast(buffMessages.Where(k => !k.Bane).ToList().GroupBy(m => m.Spell.TargetEffect).Select(a => a.First().LandblockMessage).ToArray()); // update server-side enchantments var buffsForPlayer = buffMessages.Where(k => !k.Bane).ToList().Select(k => k.Enchantment); var lifeBuffsForPlayer = buffsForPlayer.Where(k => k.Spell.School == MagicSchool.LifeMagic).ToList(); var critterBuffsForPlayer = buffsForPlayer.Where(k => k.Spell.School == MagicSchool.CreatureEnchantment).ToList(); var itemBuffsForPlayer = buffsForPlayer.Where(k => k.Spell.School == MagicSchool.ItemEnchantment).ToList(); lifeBuffsForPlayer.ForEach(spl => { CreateEnchantmentSilent(spl.Spell, targetPlayer); }); critterBuffsForPlayer.ForEach(spl => { CreateEnchantmentSilent(spl.Spell, targetPlayer); }); itemBuffsForPlayer.ForEach(spl => { CreateEnchantmentSilent(spl.Spell, targetPlayer); }); } if (buffMessages.Any(k => k.Bane)) { // Impen/bane var items = targetPlayer.EquippedObjects.Values.ToList(); var itembuffs = buffMessages.Where(k => k.Bane).ToList(); foreach (var itemBuff in itembuffs) { foreach (var item in items) { if ((item.WeenieType == WeenieType.Clothing || item.IsShield) && item.IsEnchantable) CreateEnchantmentSilent(itemBuff.Spell, item); } } } }); } private void CreateEnchantmentSilent(Spell spell, WorldObject target) { var addResult = target.EnchantmentManager.Add(spell, this, null); if (target is Player targetPlayer) { targetPlayer.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(targetPlayer.Session, new Enchantment(targetPlayer, addResult.Enchantment))); targetPlayer.HandleSpellHooks(spell); } } // TODO: switch this over to SpellProgressionTables private static string[] Buffs = new string[] { #region spells // @ indicates impenetrability or a bane "Strength", "Invulnerability", "FireProtection", "Armor", "Rejuvenation", "Regeneration", "ManaRenewal", "Impregnability", "MagicResistance", //"AxeMastery", // light weapons "LightWeaponsMastery", //"DaggerMastery", // finesse weapons "FinesseWeaponsMastery", //"MaceMastery", //"SpearMastery", //"StaffMastery", //"SwordMastery", // heavy weapons "HeavyWeaponsMastery", //"UnarmedCombatMastery", //"BowMastery", // missile weapons "MissileWeaponsMastery", //"CrossbowMastery", //"ThrownWeaponMastery", "AcidProtection", "CreatureEnchantmentMastery", "ItemEnchantmentMastery", "LifeMagicMastery", "WarMagicMastery", "ManaMastery", "ArcaneEnlightenment", "ArcanumSalvaging", "ArmorExpertise", "ItemExpertise", "MagicItemExpertise", "WeaponExpertise", "MonsterAttunement", "PersonAttunement", "DeceptionMastery", "HealingMastery", "LeadershipMastery", "LockpickMastery", "Fealty", "JumpingMastery", "Sprint", "BludgeonProtection", "ColdProtection", "LightningProtection", "BladeProtection", "PiercingProtection", "Endurance", "Coordination", "Quickness", "Focus", "Willpower", "CookingMastery", "FletchingMastery", "AlchemyMastery", "VoidMagicMastery", "SummoningMastery", "SwiftKiller", "Defender", "BloodDrinker", "HeartSeeker", "HermeticLink", "SpiritDrinker", "DualWieldMastery", "TwoHandedMastery", "DirtyFightingMastery", "RecklessnessMastery", "SneakAttackMastery", "ShieldMastery", "@Impenetrability", "@PiercingBane", "@BludgeonBane", "@BladeBane", "@AcidBane", "@FlameBane", "@FrostBane", "@LightningBane", #endregion }; private class BuffMessage { public bool Bane { get; set; } = false; public GameEventMagicUpdateEnchantment SessionMessage { get; set; } = null; public GameMessageScript LandblockMessage { get; set; } = null; public Spell Spell { get; set; } = null; public Enchantment Enchantment { get; set; } = null; public void SetTargetPlayer(Player p) { Enchantment.Target = p; SessionMessage = new GameEventMagicUpdateEnchantment(p.Session, Enchantment); SetLandblockMessage(p.Guid); } public void SetLandblockMessage(ObjectGuid target) { LandblockMessage = new GameMessageScript(target, Spell.TargetEffect, 1f); } } private static BuffMessage BuildBuffMessage(uint spellID) { BuffMessage buff = new BuffMessage(); buff.Spell = new Spell(spellID); if (buff.Spell.NotFound) return null; buff.Enchantment = new Enchantment(null, 0, spellID, 1, (EnchantmentMask)buff.Spell.StatModType, buff.Spell.StatModVal); return buff; } public void HandleSpellbookFilters(SpellBookFilterOptions filters) { Character.SpellbookFilters = (uint)filters; } public void HandleSetDesiredComponentLevel(uint component_wcid, uint amount) { // ensure wcid is spell component if (!SpellComponent.IsValid(component_wcid)) { log.Warn($"{Name}.HandleSetDesiredComponentLevel({component_wcid}, {amount}): invalid spell component wcid"); return; } if (amount > 0) { var existing = Character.GetFillComponent(component_wcid, CharacterDatabaseLock); if (existing == null) Character.AddFillComponent(component_wcid, amount, CharacterDatabaseLock, out bool exists); else existing.QuantityToRebuy = (int)amount; } else Character.TryRemoveFillComponent(component_wcid, out var _, CharacterDatabaseLock); CharacterChangesDetected = true; } public static Dictionary FociWCIDs = new Dictionary() { { MagicSchool.CreatureEnchantment, 15268 }, // Foci of Enchantment { MagicSchool.ItemEnchantment, 15269 }, // Foci of Artifice { MagicSchool.LifeMagic, 15270 }, // Foci of Verdancy { MagicSchool.WarMagic, 15271 }, // Foci of Strife { MagicSchool.VoidMagic, 43173 }, // Foci of Shadow }; public bool HasFoci(MagicSchool school) { switch (school) { case MagicSchool.CreatureEnchantment: if (AugmentationInfusedCreatureMagic > 0) return true; break; case MagicSchool.ItemEnchantment: if (AugmentationInfusedItemMagic > 0) return true; break; case MagicSchool.LifeMagic: if (AugmentationInfusedLifeMagic > 0) return true; break; case MagicSchool.VoidMagic: if (AugmentationInfusedVoidMagic > 0) return true; break; case MagicSchool.WarMagic: if (AugmentationInfusedWarMagic > 0) return true; break; } var wcid = FociWCIDs[school]; return Inventory.Values.FirstOrDefault(i => i.WeenieClassId == wcid) != null; } public void HandleSpellHooks(Spell spell) { HandleMaxVitalUpdate(spell); // unsure if spell hook was here in retail, // but this has the potential to take the client out of autorun mode // which causes them to stop if they hit a turn key afterwards if (PropertyManager.GetBool("runrate_add_hooks").Item) HandleRunRateUpdate(spell); } /// /// Called when an enchantment is added or removed, /// checks if the spell affects the max vitals, /// and if so, updates the client immediately /// public void HandleMaxVitalUpdate(Spell spell) { var maxVitals = spell.UpdatesMaxVitals; if (maxVitals.Count == 0) return; var actionChain = new ActionChain(); actionChain.AddDelaySeconds(1.0f); // client needs time for primary attribute updates actionChain.AddAction(this, () => { foreach (var maxVital in maxVitals) { var playerVital = Vitals[maxVital]; Session.Network.EnqueueSend(new GameMessagePrivateUpdateAttribute2ndLevel(this, playerVital.ToEnum(), playerVital.Current)); } }); actionChain.EnqueueChain(); } public bool HandleRunRateUpdate(Spell spell) { if (!spell.UpdatesRunRate) return false; return HandleRunRateUpdate(); } public void AuditItemSpells() { // cleans up bugged chars with dangling item set spells // from previous bugs var allPossessions = GetAllPossessions().ToDictionary(i => i.Guid, i => i); // this is a legacy method, but is still a decent failsafe to catch any existing issues // get active item enchantments var enchantments = Biota.PropertiesEnchantmentRegistry.Clone(BiotaDatabaseLock).Where(i => i.Duration == -1 && i.SpellId != (int)SpellId.Vitae).ToList(); foreach (var enchantment in enchantments) { var table = enchantment.HasSpellSetId ? allPossessions : EquippedObjects; // if this item is not equipped, remove enchantment if (!table.TryGetValue(new ObjectGuid(enchantment.CasterObjectId), out var item)) { var spell = new Spell(enchantment.SpellId, false); log.Error($"{Name}.AuditItemSpells(): removing spell {spell.Name} from {(enchantment.HasSpellSetId ? "non-possessed" : "non-equipped")} item"); EnchantmentManager.Dispel(enchantment); continue; } // is this item part of a set? if (!item.HasItemSet) continue; // get all of the equipped items in this set var setItems = EquippedObjects.Values.Where(i => i.HasItemSet && i.EquipmentSetId == item.EquipmentSetId).ToList(); // get all of the spells currently active from this set var currentSpells = GetSpellSet(setItems); // get all of the spells possible for this item set var possibleSpells = GetSpellSetAll((EquipmentSet)item.EquipmentSetId); // get the difference between them var inactiveSpells = possibleSpells.Except(currentSpells).ToList(); // remove any item set spells that shouldn't be active foreach (var inactiveSpell in inactiveSpells) { var removeSpells = enchantments.Where(i => i.SpellSetId == item.EquipmentSetId && i.SpellId == inactiveSpell.Id).ToList(); foreach (var removeSpell in removeSpells) { log.Error($"{Name}.AuditItemSpells(): removing spell {inactiveSpell.Name} from {item.EquipmentSetId}"); EnchantmentManager.Dispel(removeSpell); } } } } } }