From a5b8efd032f4eb6113bd61697131c602c6c8e6eb Mon Sep 17 00:00:00 2001 From: hondacrx Date: Sat, 3 Feb 2024 13:11:01 -0500 Subject: [PATCH] Core/Spells: Implement spell queue Port From (https://github.com/TrinityCore/TrinityCore/commit/27019a62a4294f8dd48d975f85b1907c5adee0e3) --- Source/Game/Entities/Player/Player.Fields.cs | 1 + Source/Game/Entities/Player/Player.Spells.cs | 267 +++++++++++++++++- Source/Game/Entities/Player/Player.cs | 7 + Source/Game/Handlers/SpellHandler.cs | 201 +++---------- .../Game/Networking/Packets/SpellPackets.cs | 45 +-- Source/Game/Networking/Packets/ToyPackets.cs | 2 +- Source/Game/Spells/Spell.cs | 2 + Source/Game/Spells/SpellCastRequest.cs | 37 +++ Source/Game/Spells/SpellCastTargets.cs | 2 +- Source/Game/Spells/SpellHistory.cs | 18 +- 10 files changed, 390 insertions(+), 192 deletions(-) create mode 100644 Source/Game/Spells/SpellCastRequest.cs diff --git a/Source/Game/Entities/Player/Player.Fields.cs b/Source/Game/Entities/Player/Player.Fields.cs index 93cfbb413..15ca13b60 100644 --- a/Source/Game/Entities/Player/Player.Fields.cs +++ b/Source/Game/Entities/Player/Player.Fields.cs @@ -110,6 +110,7 @@ namespace Game.Entities public Spell m_spellModTakingSpell; uint m_oldpetspell; Dictionary m_storedAuraTeleportLocations = new(); + SpellCastRequest _pendingSpellCastRequest; //Mail List m_mail = new(); diff --git a/Source/Game/Entities/Player/Player.Spells.cs b/Source/Game/Entities/Player/Player.Spells.cs index 49b8097fd..50a0ff211 100644 --- a/Source/Game/Entities/Player/Player.Spells.cs +++ b/Source/Game/Entities/Player/Player.Spells.cs @@ -1433,7 +1433,7 @@ namespace Game.Entities return 0; } - + int SkillGainChance(uint SkillValue, uint GrayLevel, uint GreenLevel, uint YellowLevel) { if (SkillValue >= GrayLevel) @@ -1702,7 +1702,7 @@ namespace Game.Entities return -1; } - + int FindEmptyProfessionSlotFor(uint skillId) { SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(skillId); @@ -3759,6 +3759,269 @@ namespace Game.Entities SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillPermBonus, (int)pos), bonus); } + + public void RequestSpellCast(SpellCastRequest castRequest) + { + // We are overriding an already existing spell cast request so inform the client that the old cast is being replaced + if (_pendingSpellCastRequest != null) + CancelPendingCastRequest(); + + _pendingSpellCastRequest = castRequest; + + // If we can process the cast request right now, do it. + if (CanExecutePendingSpellCastRequest()) + ExecutePendingSpellCastRequest(); + } + + public void CancelPendingCastRequest() + { + if (_pendingSpellCastRequest == null) + return; + + // We have to inform the client that the cast has been canceled. Otherwise the cast button will remain highlightened + CastFailed castFailed = new(); + castFailed.CastID = _pendingSpellCastRequest.CastRequest.CastID; + castFailed.SpellID = (int)_pendingSpellCastRequest.CastRequest.SpellID; + castFailed.Reason = SpellCastResult.DontReport; + SendPacket(castFailed); + + _pendingSpellCastRequest = null; + } + + // A spell can be queued up within 400 milliseconds before global cooldown expires or the cast finishes + static TimeSpan SPELL_QUEUE_TIME_WINDOW = TimeSpan.FromMilliseconds(400); + + public bool CanRequestSpellCast(SpellInfo spellInfo, Unit castingUnit) + { + if (castingUnit.GetSpellHistory().GetRemainingGlobalCooldown(spellInfo) > SPELL_QUEUE_TIME_WINDOW) + return false; + + foreach (CurrentSpellTypes spellSlot in new[] { CurrentSpellTypes.Melee, CurrentSpellTypes.Generic }) + { + Spell spell = GetCurrentSpell(spellSlot); + if (spell != null && TimeSpan.FromMilliseconds(spell.GetRemainingCastTime()) > SPELL_QUEUE_TIME_WINDOW) + return false; + } + + return true; + } + + void ExecutePendingSpellCastRequest() + { + if (_pendingSpellCastRequest == null) + return; + + TriggerCastFlags triggerFlag = TriggerCastFlags.None; + + Unit castingUnit = _pendingSpellCastRequest.CastingUnitGUID == GetGUID() ? this : Global.ObjAccessor.GetUnit(this, _pendingSpellCastRequest.CastingUnitGUID); + + // client provided targets + SpellCastTargets targets = new(castingUnit, _pendingSpellCastRequest.CastRequest); + + // The spell cast has been requested by using an item. Handle the cast accordingly. + if (_pendingSpellCastRequest.ItemData != null) + { + if (ProcessItemCast(_pendingSpellCastRequest, targets)) + _pendingSpellCastRequest = null; + else + CancelPendingCastRequest(); + return; + } + + // check known spell or raid marker spell (which not requires player to know it) + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(_pendingSpellCastRequest.CastRequest.SpellID, GetMap().GetDifficultyID()); + Player plrCaster = castingUnit.ToPlayer(); + if (plrCaster != null && !plrCaster.HasActiveSpell(spellInfo.Id) && !spellInfo.HasAttribute(SpellAttr8.SkipIsKnownCheck)) + { + bool allow = false; + + // allow casting of unknown spells for special lock cases + GameObject go = targets.GetGOTarget(); + if (go != null && go.GetSpellForLock(plrCaster) == spellInfo) + allow = true; + + // allow casting of spells triggered by clientside periodic trigger auras + if (castingUnit.HasAuraTypeWithTriggerSpell(AuraType.PeriodicTriggerSpellFromClient, spellInfo.Id)) + { + allow = true; + triggerFlag = TriggerCastFlags.FullMask; + } + + if (!allow) + { + CancelPendingCastRequest(); + return; + } + } + + // Check possible spell cast overrides + spellInfo = castingUnit.GetCastSpellInfo(spellInfo, triggerFlag); + if (spellInfo.IsPassive()) + { + CancelPendingCastRequest(); + return; + } + + // can't use our own spells when we're in possession of another unit + if (IsPossessing()) + { + CancelPendingCastRequest(); + return; + } + + // Client is resending autoshot cast opcode when other spell is cast during shoot rotation + // Skip it to prevent "interrupt" message + // Also check targets! target may have changed and we need to interrupt current spell + if (spellInfo.IsAutoRepeatRangedSpell()) + { + Spell currentSpell = castingUnit.GetCurrentSpell(CurrentSpellTypes.AutoRepeat); + if (currentSpell != null) + { + if (currentSpell.m_spellInfo == spellInfo && currentSpell.m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID()) + { + CancelPendingCastRequest(); + return; + } + } + } + + // auto-selection buff level base at target level (in spellInfo) + if (targets.GetUnitTarget() != null) + { + SpellInfo actualSpellInfo = spellInfo.GetAuraRankForLevel(targets.GetUnitTarget().GetLevelForTarget(this)); + + // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message + if (actualSpellInfo != null) + spellInfo = actualSpellInfo; + } + + Spell spell = new Spell(castingUnit, spellInfo, triggerFlag); + + SpellPrepare spellPrepare = new(); + spellPrepare.ClientCastID = _pendingSpellCastRequest.CastRequest.CastID; + spellPrepare.ServerCastID = spell.m_castId; + SendPacket(spellPrepare); + + spell.m_fromClient = true; + spell.m_misc.Data0 = _pendingSpellCastRequest.CastRequest.Misc[0]; + spell.m_misc.Data1 = _pendingSpellCastRequest.CastRequest.Misc[1]; + spell.Prepare(targets); + + _pendingSpellCastRequest = null; + } + + bool ProcessItemCast(SpellCastRequest castRequest, SpellCastTargets targets) + { + Item item = GetUseableItemByPos(castRequest.ItemData.PackSlot, castRequest.ItemData.Slot); + if (item == null) + { + SendEquipError(InventoryResult.ItemNotFound); + return false; + } + + if (item.GetGUID() != castRequest.ItemData.CastItem) + { + SendEquipError(InventoryResult.ItemNotFound); + return false; + } + + ItemTemplate proto = item.GetTemplate(); + if (proto == null) + { + SendEquipError(InventoryResult.ItemNotFound, item); + return false; + } + + // some item classes can be used only in equipped state + if (proto.GetInventoryType() != InventoryType.NonEquip && !item.IsEquipped()) + { + SendEquipError(InventoryResult.ItemNotFound, item); + return false; + } + + InventoryResult msg = CanUseItem(item); + if (msg != InventoryResult.Ok) + { + SendEquipError(msg, item, null); + return false; + } + + // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB) + if (proto.GetClass() == ItemClass.Consumable && !proto.HasFlag(ItemFlags.IgnoreDefaultArenaRestrictions) && InArena()) + { + SendEquipError(InventoryResult.NotDuringArenaMatch, item); + return false; + } + + // don't allow items banned in arena + if (proto.HasFlag(ItemFlags.NotUseableInArena) && InArena()) + { + SendEquipError(InventoryResult.NotDuringArenaMatch, item); + return false; + } + + if (IsInCombat()) + { + foreach (var effect in item.GetEffects()) + { + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)effect.SpellID, GetMap().GetDifficultyID()); + if (spellInfo != null) + { + if (!spellInfo.CanBeUsedInCombat(this)) + { + SendEquipError(InventoryResult.NotInCombat, item); + return false; + } + } + } + } + + // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory) + if (item.GetBonding() == ItemBondingType.OnUse || item.GetBonding() == ItemBondingType.OnAcquire || item.GetBonding() == ItemBondingType.Quest) + { + if (!item.IsSoulBound()) + { + item.SetState(ItemUpdateState.Changed, this); + item.SetBinding(true); + GetSession().GetCollectionMgr().AddItemAppearance(item); + } + } + + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.ItemUse); + + // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state. + if (!Global.ScriptMgr.OnItemUse(this, item, targets, castRequest.CastRequest.CastID)) + { + // no script or script not process request by self + CastItemUseSpell(item, targets, castRequest.CastRequest.CastID, castRequest.CastRequest.Misc); + } + + return true; + } + + bool CanExecutePendingSpellCastRequest() + { + if (_pendingSpellCastRequest == null) + return false; + + Unit castingUnit = _pendingSpellCastRequest.CastingUnitGUID == GetGUID() ? this : Global.ObjAccessor.GetUnit(this, _pendingSpellCastRequest.CastingUnitGUID); + if (castingUnit == null || !castingUnit.IsInWorld || (castingUnit != this && GetUnitBeingMoved() != castingUnit)) + { + // If the casting unit is no longer available, just cancel the entire spell cast request and be done with it + CancelPendingCastRequest(); + return false; + } + + // Generic and melee spells have to wait, channeled spells can be processed immediately. + if (castingUnit.GetCurrentSpell(CurrentSpellTypes.Channeled) == null && castingUnit.HasUnitState(UnitState.Casting)) + return false; + + // Waiting for the global cooldown to expire before attempting to execute the cast request + if (castingUnit.GetSpellHistory().GetRemainingGlobalCooldown(Global.SpellMgr.GetSpellInfo(_pendingSpellCastRequest.CastRequest.SpellID, GetMap().GetDifficultyID())) > TimeSpan.Zero) + return false; + + return true; + } } public class PlayerSpell diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index e0614993a..05947813c 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -358,6 +358,10 @@ namespace Game.Entities base.Update(diff); SetCanDelayTeleport(false); + // Unit::Update updates the spell history and spell states. We can now check if we can launch another pending cast. + if (CanExecutePendingSpellCastRequest()) + ExecutePendingSpellCastRequest(); + long now = GameTime.GetGameTime(); UpdatePvPFlag(now); @@ -642,6 +646,9 @@ namespace Game.Entities return; } + // clear all pending spell cast requests when dying + CancelPendingCastRequest(); + // drunken state is cleared on death SetDrunkValue(0); diff --git a/Source/Game/Handlers/SpellHandler.cs b/Source/Game/Handlers/SpellHandler.cs index d9f018c47..81826f263 100644 --- a/Source/Game/Handlers/SpellHandler.cs +++ b/Source/Game/Handlers/SpellHandler.cs @@ -20,97 +20,22 @@ namespace Game [WorldPacketHandler(ClientOpcodes.UseItem, Processing = PacketProcessing.Inplace)] void HandleUseItem(UseItem packet) { - Player user = GetPlayer(); - // ignore for remote control state - if (user.GetUnitBeingMoved() != user) + if (_player.GetUnitBeingMoved() != _player) return; - Item item = user.GetUseableItemByPos(packet.PackSlot, packet.Slot); - if (item == null) + // Skip casting invalid spells right away + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(packet.Cast.SpellID, _player.GetMap().GetDifficultyID()); + if (spellInfo == null) { - user.SendEquipError(InventoryResult.ItemNotFound); + Log.outError(LogFilter.Network, $"WorldSession::HandleUseItemOpcode: attempted to cast a non-existing spell (Id: {packet.Cast.SpellID})"); return; } - if (item.GetGUID() != packet.CastItem) - { - user.SendEquipError(InventoryResult.ItemNotFound); - return; - } - - ItemTemplate proto = item.GetTemplate(); - if (proto == null) - { - user.SendEquipError(InventoryResult.ItemNotFound, item); - return; - } - - // some item classes can be used only in equipped state - if (proto.GetInventoryType() != InventoryType.NonEquip && !item.IsEquipped()) - { - user.SendEquipError(InventoryResult.ItemNotFound, item); - return; - } - - InventoryResult msg = user.CanUseItem(item); - if (msg != InventoryResult.Ok) - { - user.SendEquipError(msg, item); - return; - } - - // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB) - if (proto.GetClass() == ItemClass.Consumable && !proto.HasFlag(ItemFlags.IgnoreDefaultArenaRestrictions) && user.InArena()) - { - user.SendEquipError(InventoryResult.NotDuringArenaMatch, item); - return; - } - - // don't allow items banned in arena - if (proto.HasFlag(ItemFlags.NotUseableInArena) && user.InArena()) - { - user.SendEquipError(InventoryResult.NotDuringArenaMatch, item); - return; - } - - if (user.IsInCombat()) - { - foreach (ItemEffectRecord effect in item.GetEffects()) - { - SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)effect.SpellID, user.GetMap().GetDifficultyID()); - if (spellInfo != null) - { - if (!spellInfo.CanBeUsedInCombat(user)) - { - user.SendEquipError(InventoryResult.NotInCombat, item); - return; - } - } - } - } - - // check also BIND_WHEN_PICKED_UP and BIND_QUEST_ITEM for .additem or .additemset case by GM (not binded at adding to inventory) - if (item.GetBonding() == ItemBondingType.OnUse || item.GetBonding() == ItemBondingType.OnAcquire || item.GetBonding() == ItemBondingType.Quest) - { - if (!item.IsSoulBound()) - { - item.SetState(ItemUpdateState.Changed, user); - item.SetBinding(true); - GetCollectionMgr().AddItemAppearance(item); - } - } - - user.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.ItemUse); - - SpellCastTargets targets = new(user, packet.Cast); - - // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state. - if (!Global.ScriptMgr.OnItemUse(user, item, targets, packet.Cast.CastID)) - { - // no script or script not process request by self - user.CastItemUseSpell(item, targets, packet.Cast.CastID, packet.Cast.Misc); - } + if (_player.CanRequestSpellCast(spellInfo, _player)) + _player.RequestSpellCast(new SpellCastRequest(packet.Cast, _player.GetGUID(), new SpellCastRequestItemData(packet.PackSlot, packet.Slot, packet.CastItem))); + else + Spell.SendCastResult(_player, spellInfo, default, packet.Cast.CastID, SpellCastResult.SpellInProgress); } [WorldPacketHandler(ClientOpcodes.OpenItem, Processing = PacketProcessing.Inplace)] @@ -279,108 +204,46 @@ namespace Game [WorldPacketHandler(ClientOpcodes.CastSpell, Processing = PacketProcessing.ThreadSafe)] void HandleCastSpell(CastSpell cast) { - // ignore for remote control state (for player case) - Unit mover = GetPlayer().GetUnitBeingMoved(); - if (mover != GetPlayer() && mover.IsTypeId(TypeId.Player)) - return; - - SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(cast.Cast.SpellID, mover.GetMap().GetDifficultyID()); + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(cast.Cast.SpellID, _player.GetMap().GetDifficultyID()); if (spellInfo == null) { - Log.outError(LogFilter.Network, "WORLD: unknown spell id {0}", cast.Cast.SpellID); + Log.outError(LogFilter.Network, $"WorldSession::HandleCastSpellOpcode: attempted to cast a non-existing spell (Id: {cast.Cast.SpellID})"); return; } - Unit caster = mover; - if (caster.IsTypeId(TypeId.Unit) && !caster.ToCreature().HasSpell(spellInfo.Id)) + // ignore for remote control state (for player case) + Unit mover = _player.GetUnitBeingMoved(); + if (mover != _player && mover.IsPlayer()) + return; + + Unit castingUnit = mover; + if (castingUnit.IsCreature() && !castingUnit.ToCreature().HasSpell(spellInfo.Id)) { // If the vehicle creature does not have the spell but it allows the passenger to cast own spells // change caster to player and let him cast - if (!GetPlayer().IsOnVehicle(caster) || spellInfo.CheckVehicle(GetPlayer()) != SpellCastResult.SpellCastOk) + if (!GetPlayer().IsOnVehicle(castingUnit) || spellInfo.CheckVehicle(GetPlayer()) != SpellCastResult.SpellCastOk) return; - caster = GetPlayer(); - } - - TriggerCastFlags triggerFlag = TriggerCastFlags.None; - - // client provided targets - SpellCastTargets targets = new(caster, cast.Cast); - - // check known spell or raid marker spell (which not requires player to know it) - if (caster.IsTypeId(TypeId.Player) && !caster.ToPlayer().HasActiveSpell(spellInfo.Id) && !spellInfo.HasAttribute(SpellAttr8.SkipIsKnownCheck)) - { - bool allow = false; - - // allow casting of unknown spells for special lock cases - GameObject go = targets.GetGOTarget(); - if (go != null) - if (go.GetSpellForLock(caster.ToPlayer()) == spellInfo) - allow = true; - - // allow casting of spells triggered by clientside periodic trigger auras - if (caster.HasAuraTypeWithTriggerSpell(AuraType.PeriodicTriggerSpellFromClient, spellInfo.Id)) - { - allow = true; - triggerFlag = TriggerCastFlags.FullMask; - } - - if (!allow) - return; - } - - // Check possible spell cast overrides - spellInfo = caster.GetCastSpellInfo(spellInfo, triggerFlag); - - if (spellInfo.IsPassive()) - return; - - // can't use our own spells when we're in possession of another unit, - if (GetPlayer().IsPossessing()) - return; - - // Client is resending autoshot cast opcode when other spell is cast during shoot rotation - // Skip it to prevent "interrupt" message - // Also check targets! target may have changed and we need to interrupt current spell - if (spellInfo.IsAutoRepeatRangedSpell()) - { - Spell autoRepeatSpell = caster.GetCurrentSpell(CurrentSpellTypes.AutoRepeat); - if (autoRepeatSpell != null) - if (autoRepeatSpell.m_spellInfo == spellInfo && autoRepeatSpell.m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID()) - return; - } - - // auto-selection buff level base at target level (in spellInfo) - if (targets.GetUnitTarget() != null) - { - SpellInfo actualSpellInfo = spellInfo.GetAuraRankForLevel(targets.GetUnitTarget().GetLevelForTarget(caster)); - - // if rank not found then function return NULL but in explicit cast case original spell can be casted and later failed with appropriate error message - if (actualSpellInfo != null) - spellInfo = actualSpellInfo; + castingUnit = GetPlayer(); } if (cast.Cast.MoveUpdate != null) HandleMovementOpcode(ClientOpcodes.MoveStop, cast.Cast.MoveUpdate); - Spell spell = new(caster, spellInfo, triggerFlag); - - SpellPrepare spellPrepare = new(); - spellPrepare.ClientCastID = cast.Cast.CastID; - spellPrepare.ServerCastID = spell.m_castId; - SendPacket(spellPrepare); - - spell.m_fromClient = true; - spell.m_misc.Data0 = cast.Cast.Misc[0]; - spell.m_misc.Data1 = cast.Cast.Misc[1]; - spell.Prepare(targets); + if (_player.CanRequestSpellCast(spellInfo, castingUnit)) + _player.RequestSpellCast(new SpellCastRequest(cast.Cast, castingUnit.GetGUID())); + else + Spell.SendCastResult(_player, spellInfo, default, cast.Cast.CastID, SpellCastResult.SpellInProgress); } [WorldPacketHandler(ClientOpcodes.CancelCast, Processing = PacketProcessing.ThreadSafe)] void HandleCancelCast(CancelCast packet) { - if (GetPlayer().IsNonMeleeSpellCast(false)) - GetPlayer().InterruptNonMeleeSpells(false, packet.SpellID, false); + if (_player.IsNonMeleeSpellCast(false)) + { + _player.InterruptNonMeleeSpells(false, packet.SpellID, false); + _player.CancelPendingCastRequest(); // canceling casts also cancels pending spell cast requests + } } [WorldPacketHandler(ClientOpcodes.CancelAura, Processing = PacketProcessing.Inplace)] @@ -486,6 +349,12 @@ namespace Game _player.InterruptSpell(CurrentSpellTypes.AutoRepeat); } + [WorldPacketHandler(ClientOpcodes.CancelChannelling, Processing = PacketProcessing.Inplace)] + void HandleCancelQueuedSpellOpcode(CancelQueuedSpell cancelQueuedSpell) + { + _player.CancelPendingCastRequest(); + } + [WorldPacketHandler(ClientOpcodes.CancelChannelling, Processing = PacketProcessing.Inplace)] void HandleCancelChanneling(CancelChannelling cancelChanneling) { diff --git a/Source/Game/Networking/Packets/SpellPackets.cs b/Source/Game/Networking/Packets/SpellPackets.cs index a532c4238..6a74413d3 100644 --- a/Source/Game/Networking/Packets/SpellPackets.cs +++ b/Source/Game/Networking/Packets/SpellPackets.cs @@ -44,7 +44,7 @@ namespace Game.Networking.Packets public int ChannelSpell; public int Reason; // 40 = /run SpellStopCasting(), 16 = movement/AURA_INTERRUPT_FLAG_MOVE, 41 = turning/AURA_INTERRUPT_FLAG_TURNING - // does not match SpellCastResult enum + // does not match SpellCastResult enum } class CancelGrowthAura : ClientPacket @@ -181,11 +181,11 @@ namespace Game.Networking.Packets public class CastSpell : ClientPacket { - public SpellCastRequest Cast; + public SpellCastRequestPkt Cast; public CastSpell(WorldPacket packet) : base(packet) { - Cast = new SpellCastRequest(); + Cast = new SpellCastRequestPkt(); } public override void Read() @@ -197,11 +197,11 @@ namespace Game.Networking.Packets public class PetCastSpell : ClientPacket { public ObjectGuid PetGUID; - public SpellCastRequest Cast; + public SpellCastRequestPkt Cast; public PetCastSpell(WorldPacket packet) : base(packet) { - Cast = new SpellCastRequest(); + Cast = new SpellCastRequestPkt(); } public override void Read() @@ -216,11 +216,11 @@ namespace Game.Networking.Packets public byte PackSlot; public byte Slot; public ObjectGuid CastItem; - public SpellCastRequest Cast; + public SpellCastRequestPkt Cast; public UseItem(WorldPacket packet) : base(packet) { - Cast = new SpellCastRequest(); + Cast = new SpellCastRequestPkt(); } public override void Read() @@ -363,8 +363,8 @@ namespace Game.Networking.Packets public int SpellID; public SpellCastResult Reason; public int FailedArg1 = -1; - public int FailedArg2 = -1; - + public int FailedArg2 = -1; + public CastFailedBase(ServerOpcodes opcode, ConnectionType connectionType) : base(opcode, connectionType) { } public override void Write() @@ -760,7 +760,7 @@ namespace Game.Networking.Packets _worldPacket.WriteInt32(Delay); } } - + public class CancelCast : ClientPacket { public CancelCast(WorldPacket packet) : base(packet) { } @@ -1161,7 +1161,14 @@ namespace Game.Networking.Packets public ushort OverrideID; } - + + class CancelQueuedSpell : ClientPacket + { + public CancelQueuedSpell(WorldPacket packet) : base(packet) { } + + public override void Read() { } + } + //Structs public struct SpellLogPowerData { @@ -1326,14 +1333,14 @@ namespace Game.Networking.Packets } public void Write(WorldPacket data) - { + { data.WriteFloat(PlayerItemLevel); data.WriteFloat(TargetItemLevel); data.WriteInt16(PlayerLevelDelta); data.WriteUInt32(ScalingHealthItemLevelCurveID); data.WriteUInt8(TargetLevel); data.WriteUInt8(Expansion); - data.WriteInt8(TargetScalingLevelDelta); + data.WriteInt8(TargetScalingLevelDelta); data.WriteUInt32((uint)Flags); data.WriteUInt32(PlayerContentTuningID); data.WriteUInt32(TargetContentTuningID); @@ -1356,7 +1363,7 @@ namespace Game.Networking.Packets public int Unused927; public enum ContentTuningType - { + { CreatureToPlayerDamage = 1, PlayerToCreatureDamage = 2, CreatureToCreatureDamage = 4, @@ -1505,7 +1512,7 @@ namespace Game.Networking.Packets { public void Write(WorldPacket data) { - data .WriteUInt8(Slot); + data.WriteUInt8(Slot); data.WriteBit(AuraData != null); data.FlushBits(); @@ -1661,8 +1668,8 @@ namespace Game.Networking.Packets } } - public class SpellCastRequest - { + public class SpellCastRequestPkt + { public ObjectGuid CastID; public uint SpellID; public SpellCastVisual Visual; @@ -1852,7 +1859,7 @@ namespace Game.Networking.Packets Immunities.Write(data); Predict.Write(data); - + data.WriteBits(HitTargets.Count, 16); data.WriteBits(MissTargets.Count, 16); data.WriteBits(HitStatus.Count, 16); @@ -2000,7 +2007,7 @@ namespace Game.Networking.Packets data.FlushBits(); if (unused622_1.HasValue) - data .WriteUInt32(unused622_1.Value); + data.WriteUInt32(unused622_1.Value); if (unused622_2.HasValue) data.WriteUInt32(unused622_2.Value); } diff --git a/Source/Game/Networking/Packets/ToyPackets.cs b/Source/Game/Networking/Packets/ToyPackets.cs index e98b01f93..febd6af54 100644 --- a/Source/Game/Networking/Packets/ToyPackets.cs +++ b/Source/Game/Networking/Packets/ToyPackets.cs @@ -29,7 +29,7 @@ namespace Game.Networking.Packets Cast.Read(_worldPacket); } - public SpellCastRequest Cast = new(); + public SpellCastRequestPkt Cast = new(); } class AccountToyUpdate : ServerPacket diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 9566c4ad6..7986fa29d 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -8121,6 +8121,8 @@ namespace Game.Spells return m_casttime; } + public int GetRemainingCastTime() { return m_timer; } + bool IsAutoRepeat() { return m_autoRepeat; diff --git a/Source/Game/Spells/SpellCastRequest.cs b/Source/Game/Spells/SpellCastRequest.cs new file mode 100644 index 000000000..57df04f47 --- /dev/null +++ b/Source/Game/Spells/SpellCastRequest.cs @@ -0,0 +1,37 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Game.Entities; +using Game.Networking.Packets; + +namespace Game.Spells +{ + public class SpellCastRequestItemData + { + public byte PackSlot; + public byte Slot; + public ObjectGuid CastItem; + + public SpellCastRequestItemData(byte packSlot, byte slot, ObjectGuid castItem) + { + PackSlot = packSlot; + Slot = slot; + CastItem = castItem; + } + } + + public class SpellCastRequest + { + public SpellCastRequestPkt CastRequest; + public ObjectGuid CastingUnitGUID; + public SpellCastRequestItemData ItemData; + + public SpellCastRequest(SpellCastRequestPkt castRequest, ObjectGuid castingUnitGUID, SpellCastRequestItemData? itemData = null) + { + CastRequest = castRequest; + CastingUnitGUID = castingUnitGUID; + ItemData = itemData; + } + } + +} diff --git a/Source/Game/Spells/SpellCastTargets.cs b/Source/Game/Spells/SpellCastTargets.cs index 989adad17..0099b0290 100644 --- a/Source/Game/Spells/SpellCastTargets.cs +++ b/Source/Game/Spells/SpellCastTargets.cs @@ -18,7 +18,7 @@ namespace Game.Spells m_dst = new SpellDestination(); } - public SpellCastTargets(Unit caster, SpellCastRequest spellCastRequest) + public SpellCastTargets(Unit caster, SpellCastRequestPkt spellCastRequest) { m_targetMask = spellCastRequest.Target.Flags; m_objectTargetGUID = spellCastRequest.Target.Unit; diff --git a/Source/Game/Spells/SpellHistory.cs b/Source/Game/Spells/SpellHistory.cs index 556180cdf..e77a5be0f 100644 --- a/Source/Game/Spells/SpellHistory.cs +++ b/Source/Game/Spells/SpellHistory.cs @@ -537,7 +537,7 @@ namespace Game.Spells } if (cooldownEntry.CooldownEnd <= now) - { + { _categoryCooldowns.Remove(cooldownEntry.CategoryId); _spellCooldowns.Remove(cooldownEntry.SpellId); } @@ -697,7 +697,7 @@ namespace Game.Spells { return GetRemainingCategoryCooldown(spellInfo.GetCategory()); } - + public void LockSpellSchool(SpellSchoolMask schoolMask, TimeSpan lockoutTime) { DateTime now = GameTime.GetSystemTime(); @@ -921,6 +921,18 @@ namespace Game.Spells _globalCooldowns[spellInfo.StartRecoveryCategory] = new DateTime(); } + public TimeSpan GetRemainingGlobalCooldown(SpellInfo spellInfo) + { + if (!_globalCooldowns.TryGetValue(spellInfo.StartRecoveryCategory, out DateTime end)) + return TimeSpan.Zero; + + DateTime now = GameTime.GetDateAndTime(); + if (end < now) + return TimeSpan.Zero; + + return end - now; + } + public Player GetPlayerOwner() { return _owner.GetCharmerOrOwnerPlayerOrPlayerItself(); @@ -952,7 +964,7 @@ namespace Game.Spells player.SendPacket(setSpellCharges); } } - + void GetCooldownDurations(SpellInfo spellInfo, uint itemId, ref uint categoryId) { TimeSpan notUsed = TimeSpan.Zero;