diff --git a/Code/client/Events/RemoveSpellEvent.h b/Code/client/Events/RemoveSpellEvent.h new file mode 100644 index 000000000..0d0e3e0eb --- /dev/null +++ b/Code/client/Events/RemoveSpellEvent.h @@ -0,0 +1,9 @@ +#pragma once + +struct RemoveSpellEvent +{ + RemoveSpellEvent() = default; + + uint32_t TargetId{}; + uint32_t SpellId{}; +}; diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 536b1f50b..fbe577fc7 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -352,6 +352,19 @@ void Actor::StopCombat() noexcept s_pStopCombat(this); } +void Actor::RemoveSpell(MagicItem* apSpell) noexcept +{ + TP_THIS_FUNCTION(TRemoveSpell, void, Actor, MagicItem*); + POINTER_SKYRIMSE(TRemoveSpell, removeSpell, 38717); + if (!apSpell) + { + spdlog::error("Actor::RemoveSpell: apSpell is null"); + return; + } + // spdlog::info("Removing spell: {} from actor: {}", apSpell->formID, formID); + TiltedPhoques::ThisCall(removeSpell, this, apSpell); +} + bool Actor::HasPerk(uint32_t aPerkFormId) const noexcept { return GetPerkRank(aPerkFormId) != 0; diff --git a/Code/client/Games/Skyrim/Actor.h b/Code/client/Games/Skyrim/Actor.h index ba9effc70..077559357 100644 --- a/Code/client/Games/Skyrim/Actor.h +++ b/Code/client/Games/Skyrim/Actor.h @@ -250,6 +250,7 @@ struct Actor : TESObjectREFR void StopCombat() noexcept; bool PlayIdle(TESIdleForm* apIdle) noexcept; void FixVampireLordModel() noexcept; + void RemoveSpell(MagicItem* apSpell) noexcept; enum ActorFlags { diff --git a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp index df017e612..97cd4b230 100644 --- a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp +++ b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp @@ -17,7 +17,10 @@ #include +#include + TP_THIS_FUNCTION(TAddTarget, bool, MagicTarget, MagicTarget::AddTargetData& arData); +TP_THIS_FUNCTION(TRemoveSpell, bool, Actor, MagicItem* apSpell); TP_THIS_FUNCTION(TCheckAddEffectTargetData, bool, MagicTarget::AddTargetData, void* arArgs, float afResistance); TP_THIS_FUNCTION(TFindTargets, bool, MagicCaster, float afEffectivenessMult, int32_t* aruiTargetCount, TESBoundObject* apSource, char abLoadCast, char abAdjust); @@ -26,6 +29,7 @@ TP_THIS_FUNCTION(THasPerk, bool, Actor, TESForm* apPerk, void* apUnk1, double* a TP_THIS_FUNCTION(TGetPerkRank, uint8_t, Actor, TESForm* apPerk); static TAddTarget* RealAddTarget = nullptr; +static TRemoveSpell* RealRemoveSpell = nullptr; static TCheckAddEffectTargetData* RealCheckAddEffectTargetData = nullptr; static TFindTargets* RealFindTargets = nullptr; static TAdjustForPerks* RealAdjustForPerks = nullptr; @@ -181,6 +185,26 @@ bool TP_MAKE_THISCALL(HookAddTarget, MagicTarget, MagicTarget::AddTargetData& ar } } +// Designed to run when we hook removespell (AddressLib ID is 38717) +// Sends a message to the server to remove the spell from this player on other clients +bool TP_MAKE_THISCALL(HookRemoveSpell, Actor, MagicItem* apSpell) +{ + bool result = TiltedPhoques::ThisCall(RealRemoveSpell, apThis, apSpell); + if (apThis->GetExtension()->IsLocalPlayer() && result) + { + // Log spell info + //spdlog::info("Removing spell {}, ID: {} from local player", apSpell->GetName() , apSpell->formID); + RemoveSpellEvent removalEvent; + + removalEvent.TargetId = apThis->formID; + removalEvent.SpellId = apSpell->formID; + World::Get().GetRunner().Trigger(removalEvent); + } + + return result; +} + + bool TP_MAKE_THISCALL(HookCheckAddEffectTargetData, MagicTarget::AddTargetData, void* arArgs, float afResistance) { if (s_autoSucceedEffectCheck) @@ -227,6 +251,7 @@ uint8_t TP_MAKE_THISCALL(HookGetPerkRank, Actor, TESForm* apPerk) static TiltedPhoques::Initializer s_magicTargetHooks([]() { POINTER_SKYRIMSE(TAddTarget, addTarget, 34526); + POINTER_SKYRIMSE(TRemoveSpell, removeSpell, 38717); POINTER_SKYRIMSE(TCheckAddEffectTargetData, checkAddEffectTargetData, 34525); POINTER_SKYRIMSE(TFindTargets, findTargets, 34410); POINTER_SKYRIMSE(TAdjustForPerks, adjustForPerks, 34053); @@ -234,6 +259,7 @@ static TiltedPhoques::Initializer s_magicTargetHooks([]() { POINTER_SKYRIMSE(TGetPerkRank, getPerkRank, 37698); RealAddTarget = addTarget.Get(); + RealRemoveSpell = removeSpell.Get(); RealCheckAddEffectTargetData = checkAddEffectTargetData.Get(); RealFindTargets = findTargets.Get(); RealAdjustForPerks = adjustForPerks.Get(); @@ -241,6 +267,7 @@ static TiltedPhoques::Initializer s_magicTargetHooks([]() { RealGetPerkRank = getPerkRank.Get(); TP_HOOK(&RealAddTarget, HookAddTarget); + TP_HOOK(&RealRemoveSpell, HookRemoveSpell); TP_HOOK(&RealCheckAddEffectTargetData, HookCheckAddEffectTargetData); TP_HOOK(&RealFindTargets, HookFindTargets); TP_HOOK(&RealAdjustForPerks, HookAdjustForPerks); diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index de9103ac5..e6d2c59c8 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -6,6 +6,9 @@ #include #include #include +#include + +#include #include #include @@ -40,6 +43,8 @@ MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher, Transpo m_notifyInterruptCastConnection = m_dispatcher.sink().connect<&MagicService::OnNotifyInterruptCast>(this); m_addTargetEventConnection = m_dispatcher.sink().connect<&MagicService::OnAddTargetEvent>(this); m_notifyAddTargetConnection = m_dispatcher.sink().connect<&MagicService::OnNotifyAddTarget>(this); + m_removeSpellEventConnection = m_dispatcher.sink().connect<&MagicService::OnRemoveSpellEvent>(this); + m_notifyRemoveSpell = m_dispatcher.sink().connect<&MagicService::OnNotifyRemoveSpell>(this); } void MagicService::OnUpdate(const UpdateEvent& acEvent) noexcept @@ -409,6 +414,75 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept spdlog::debug("Applied remote magic effect"); } +void MagicService::OnRemoveSpellEvent(const RemoveSpellEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return; + + RemoveSpellRequest request{}; + + if (!m_world.GetModSystem().GetServerModId(acEvent.SpellId, request.SpellId.ModId, request.SpellId.BaseId)) + { + spdlog::error("{}: Could not find spell with form {:X}", __FUNCTION__, acEvent.SpellId); + return; + } + + auto view = m_world.view(); + const auto it = std::find_if(std::begin(view), std::end(view), [id = acEvent.TargetId, view](auto entity) { + return view.get(entity).Id == id; + }); + + if (it == std::end(view)) + { + spdlog::warn("Form id not found for magic remove target, form id: {:X}", acEvent.TargetId); + return; + } + + std::optional serverIdRes = Utils::GetServerId(*it); + if (!serverIdRes.has_value()) + { + spdlog::warn("Server id not found for magic remove target, form id: {:X}", acEvent.TargetId); + return; + } + + request.TargetId = serverIdRes.value(); + + //spdlog::info("Requesting remove spell with base id {:X} from actor with server id {:X}", request.SpellId.BaseId, request.TargetId); + + m_transport.Send(request); +} + +void MagicService::OnNotifyRemoveSpell(const NotifyRemoveSpell& acMessage) noexcept +{ + uint32_t targetFormId = acMessage.TargetId; + + Actor* pActor = Utils::GetByServerId(acMessage.TargetId); + if (!pActor) + { + spdlog::warn(__FUNCTION__ ": could not find actor server id {:X}", acMessage.TargetId); + return; + } + + const uint32_t cSpellId = World::Get().GetModSystem().GetGameId(acMessage.SpellId); + if (cSpellId == 0) + { + spdlog::error("{}: failed to retrieve spell id, GameId base: {:X}, mod: {:X}", __FUNCTION__, + acMessage.SpellId.BaseId, acMessage.SpellId.ModId); + return; + } + + MagicItem* pSpell = Cast(TESForm::GetById(cSpellId)); + if (!pSpell) + { + spdlog::error("{}: Failed to retrieve spell by id {:X}", __FUNCTION__, cSpellId); + return; + } + + // Remove the spell from the actor + //spdlog::info("Removing spell with form id {:X} from actor with form id {:X}", cSpellId, targetFormId); + pActor->RemoveSpell(pSpell); +} + void MagicService::ApplyQueuedEffects() noexcept { static std::chrono::steady_clock::time_point lastSendTimePoint; diff --git a/Code/client/Services/MagicService.h b/Code/client/Services/MagicService.h index 034b267b5..35620b080 100644 --- a/Code/client/Services/MagicService.h +++ b/Code/client/Services/MagicService.h @@ -4,6 +4,7 @@ #include #include #include +#include struct World; struct TransportService; @@ -12,6 +13,7 @@ struct UpdateEvent; struct SpellCastEvent; struct InterruptCastEvent; struct AddTargetEvent; +struct RemoveSpellEvent; struct NotifySpellCast; struct NotifyInterruptCast; @@ -62,6 +64,14 @@ struct MagicService * @brief Applies a magic effect based on a server message. */ void OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept; + /** + * @brief Sends a message to remove a spell from a player. + */ + void OnRemoveSpellEvent(const RemoveSpellEvent& acEvent) noexcept; + /* + * @brief Handles removal of a spell + */ + void OnNotifyRemoveSpell(const NotifyRemoveSpell& acMessage) noexcept; private: /** @@ -97,4 +107,6 @@ struct MagicService entt::scoped_connection m_notifyInterruptCastConnection; entt::scoped_connection m_addTargetEventConnection; entt::scoped_connection m_notifyAddTargetConnection; + entt::scoped_connection m_removeSpellEventConnection; + entt::scoped_connection m_notifyRemoveSpell; }; diff --git a/Code/encoding/Messages/ClientMessageFactory.h b/Code/encoding/Messages/ClientMessageFactory.h index 9c90ad351..30e7984cd 100644 --- a/Code/encoding/Messages/ClientMessageFactory.h +++ b/Code/encoding/Messages/ClientMessageFactory.h @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -66,10 +67,10 @@ struct ClientMessageFactory { auto s_visitor = CreateMessageVisitor< AuthenticationRequest, AssignCharacterRequest, CancelAssignmentRequest, ClientReferencesMoveRequest, EnterInteriorCellRequest, RequestInventoryChanges, RequestFactionsChanges, RequestQuestUpdate, PartyInviteRequest, PartyAcceptInviteRequest, PartyLeaveRequest, PartyCreateRequest, - PartyChangeLeaderRequest, PartyKickRequest, RequestActorValueChanges, RequestActorMaxValueChanges, EnterExteriorCellRequest, RequestHealthChangeBroadcast, ActivateRequest, LockChangeRequest, AssignObjectsRequest, RequestDeathStateChange, ShiftGridCellRequest, RequestOwnershipTransfer, - RequestOwnershipClaim, RequestObjectInventoryChanges, SpellCastRequest, ProjectileLaunchRequest, InterruptCastRequest, AddTargetRequest, ScriptAnimationRequest, DrawWeaponRequest, MountRequest, NewPackageRequest, RequestRespawn, SyncExperienceRequest, RequestEquipmentChanges, - SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, RequestRemoveWaypoint, - SetTimeCommandRequest>; + PartyChangeLeaderRequest, PartyKickRequest, RequestActorValueChanges, RequestActorMaxValueChanges, EnterExteriorCellRequest, RequestHealthChangeBroadcast, ActivateRequest, LockChangeRequest, AssignObjectsRequest, RequestDeathStateChange, ShiftGridCellRequest, + RequestOwnershipTransfer, RequestOwnershipClaim, RequestObjectInventoryChanges, SpellCastRequest, ProjectileLaunchRequest, InterruptCastRequest, AddTargetRequest, ScriptAnimationRequest, DrawWeaponRequest, MountRequest, NewPackageRequest, RequestRespawn, SyncExperienceRequest, + RequestEquipmentChanges, SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, + RequestRemoveWaypoint, RemoveSpellRequest, SetTimeCommandRequest>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/NotifyRemoveSpell.cpp b/Code/encoding/Messages/NotifyRemoveSpell.cpp new file mode 100644 index 000000000..c3ddd37a5 --- /dev/null +++ b/Code/encoding/Messages/NotifyRemoveSpell.cpp @@ -0,0 +1,13 @@ +#include "NotifyRemoveSpell.h" + +void NotifyRemoveSpell::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, TargetId); + SpellId.Serialize(aWriter); +} + +void NotifyRemoveSpell::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + TargetId = Serialization::ReadVarInt(aReader); + SpellId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyRemoveSpell.h b/Code/encoding/Messages/NotifyRemoveSpell.h new file mode 100644 index 000000000..640bcdd6b --- /dev/null +++ b/Code/encoding/Messages/NotifyRemoveSpell.h @@ -0,0 +1,24 @@ +#pragma once +#include "Message.h" +#include + +struct NotifyRemoveSpell final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyRemoveSpell; + + NotifyRemoveSpell() : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyRemoveSpell& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && TargetId == acRhs.TargetId && SpellId == acRhs.SpellId; + } + + uint32_t TargetId{}; + GameId SpellId{}; +}; diff --git a/Code/encoding/Messages/RemoveSpellRequest.cpp b/Code/encoding/Messages/RemoveSpellRequest.cpp new file mode 100644 index 000000000..4357d9121 --- /dev/null +++ b/Code/encoding/Messages/RemoveSpellRequest.cpp @@ -0,0 +1,14 @@ +#include "EncodingPch.h" +#include "RemoveSpellRequest.h" + +void RemoveSpellRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, TargetId); + SpellId.Serialize(aWriter); +} + +void RemoveSpellRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + TargetId = Serialization::ReadVarInt(aReader); + SpellId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RemoveSpellRequest.h b/Code/encoding/Messages/RemoveSpellRequest.h new file mode 100644 index 000000000..8f91dc3c1 --- /dev/null +++ b/Code/encoding/Messages/RemoveSpellRequest.h @@ -0,0 +1,16 @@ +#pragma once +#include "Message.h" +#include + +struct RemoveSpellRequest final: ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestRemoveSpell; + RemoveSpellRequest() : ClientMessage(Opcode) {} + virtual ~RemoveSpellRequest() = default; + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + bool operator==(const RemoveSpellRequest& achRhs) const noexcept { return TargetId == achRhs.TargetId && SpellId == achRhs.SpellId && Opcode == achRhs.Opcode; } + + uint32_t TargetId{}; + GameId SpellId{}; +}; diff --git a/Code/encoding/Messages/ServerMessageFactory.h b/Code/encoding/Messages/ServerMessageFactory.h index a1db0d4eb..942c30fec 100644 --- a/Code/encoding/Messages/ServerMessageFactory.h +++ b/Code/encoding/Messages/ServerMessageFactory.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -73,7 +74,7 @@ struct ServerMessageFactory NotifyActorValueChanges, NotifyPartyJoined, NotifyPartyLeft, NotifyActorMaxValueChanges, NotifyHealthChangeBroadcast, NotifySpawnData, NotifyActivate, NotifyLockChange, AssignObjectsResponse, NotifyDeathStateChange, NotifyOwnershipTransfer, NotifyObjectInventoryChanges, NotifySpellCast, NotifyProjectileLaunch, NotifyInterruptCast, NotifyAddTarget, NotifyScriptAnimation, NotifyDrawWeapon, NotifyMount, NotifyNewPackage, NotifyRespawn, NotifySyncExperience, NotifyEquipmentChanges, NotifyChatMessageBroadcast, TeleportCommandResponse, NotifyPlayerRespawn, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyRelinquishControl, NotifyPlayerLeft, NotifyPlayerJoined, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, - NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult>; + NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult, NotifyRemoveSpell>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Opcodes.h b/Code/encoding/Opcodes.h index f2c69bf71..8f0b10592 100644 --- a/Code/encoding/Opcodes.h +++ b/Code/encoding/Opcodes.h @@ -31,6 +31,7 @@ enum ClientOpcode : unsigned char kSpellCastRequest, kInterruptCastRequest, kAddTargetRequest, + kRequestRemoveSpell, kProjectileLaunchRequest, kScriptAnimationRequest, kDrawWeaponRequest, @@ -85,6 +86,7 @@ enum ServerOpcode : unsigned char kNotifySpellCast, kNotifyInterruptCast, kNotifyAddTarget, + kNotifyRemoveSpell, kNotifyProjectileLaunch, kNotifyScriptAnimation, kNotifyDrawWeapon, diff --git a/Code/server/Services/MagicService.cpp b/Code/server/Services/MagicService.cpp index 435b9aa59..2901702d6 100644 --- a/Code/server/Services/MagicService.cpp +++ b/Code/server/Services/MagicService.cpp @@ -10,6 +10,7 @@ #include #include #include +#include MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) @@ -17,6 +18,7 @@ MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher) noexcep m_spellCastConnection = aDispatcher.sink>().connect<&MagicService::OnSpellCastRequest>(this); m_interruptCastConnection = aDispatcher.sink>().connect<&MagicService::OnInterruptCastRequest>(this); m_addTargetConnection = aDispatcher.sink>().connect<&MagicService::OnAddTargetRequest>(this); + m_removeSpellConnection = aDispatcher.sink>().connect<&MagicService::OnRemoveSpellRequest>(this); } void MagicService::OnSpellCastRequest(const PacketEvent& acMessage) const noexcept @@ -66,3 +68,18 @@ void MagicService::OnAddTargetRequest(const PacketEvent& acMes if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } + +void MagicService::OnRemoveSpellRequest(const PacketEvent& acMessage) const noexcept +{ + const auto& message = acMessage.Packet; + + NotifyRemoveSpell notify; + notify.TargetId = message.TargetId; + notify.SpellId = message.SpellId; + + //spdlog::info("RemoveSpellRequest: TargetId: {}, Spell baseId: {}", notify.TargetId, notify.SpellId.BaseId); + + const auto entity = static_cast(message.TargetId); + if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) + spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); +} diff --git a/Code/server/Services/MagicService.h b/Code/server/Services/MagicService.h index 670160c76..8328211be 100644 --- a/Code/server/Services/MagicService.h +++ b/Code/server/Services/MagicService.h @@ -1,6 +1,7 @@ #pragma once #include +#include struct World; struct SpellCastRequest; @@ -30,6 +31,11 @@ struct MagicService * @brief Relays magic effect messages to other clients. */ void OnAddTargetRequest(const PacketEvent& acMessage) const noexcept; + /** + * @brief Relays spell removal messages to other clients. + */ + void OnRemoveSpellRequest(const PacketEvent& acMessage) const noexcept; + private: World& m_world; @@ -37,4 +43,5 @@ struct MagicService entt::scoped_connection m_spellCastConnection; entt::scoped_connection m_interruptCastConnection; entt::scoped_connection m_addTargetConnection; + entt::scoped_connection m_removeSpellConnection; };