diff --git a/Code/client/Games/Skyrim/Forms/BGSPerk.h b/Code/client/Games/Skyrim/Forms/BGSPerk.h new file mode 100644 index 0000000000..0f18accc57 --- /dev/null +++ b/Code/client/Games/Skyrim/Forms/BGSPerk.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +struct Actor; + +struct BGSPerkEntry +{ + enum Type : uint32_t + { + kQuest = 0, + kAbility = 1, + kEntryPoint = 2, + }; + + virtual bool CheckConditionFilters(uint32_t aArgCount, void* apArgs); + virtual uint32_t GetFunction(); + virtual void* GetFunctionData(); + virtual void sub_3(); + virtual Type GetType(); + + uint8_t rank; + uint8_t priority; +}; +static_assert(offsetof(BGSPerkEntry, rank) == 0x8); +static_assert(offsetof(BGSPerkEntry, priority) == 0x9); + +struct BGSEntryPointPerkEntry : BGSPerkEntry +{ + static constexpr uint32_t kEntryPointCount = 0x5C; + + uint8_t entryPoint; +}; +static_assert(offsetof(BGSEntryPointPerkEntry, entryPoint) == 0x10); + +struct BGSPerk : TESForm +{ + uint8_t pad20[0x60 - sizeof(TESForm)]; + GameArray perkEntries; +}; +static_assert(offsetof(BGSPerk, perkEntries) == 0x60); + +// Element of TESNPC::perkRanks. PlayerCharacter stores pointers to these for the perks a player earned. +struct PerkRankData +{ + BGSPerk* pPerk; + uint8_t rank; +}; +static_assert(sizeof(PerkRankData) == 0x10); + +// The engine's visitors for Actor::VisitPerks (vtable slot 0xFA) and Actor::ForEachPerkEntry (slot 0x100). +// Returning 1 means continue. +struct PerkRankVisitor +{ + virtual uint32_t Visit(PerkRankData* apPerkRank) = 0; +}; + +struct PerkEntryVisitor +{ + virtual uint32_t Visit(BGSPerkEntry* apEntry) = 0; +}; + +// The visitor that BGSEntryPoint::HandleEntryPoint passes to Actor::ForEachPerkEntry. +struct HandleEntryPointVisitor : PerkEntryVisitor +{ + uint32_t function; + void** ppArguments; // The perk owner, followed by the entry point's filter arguments + void* pResults; + Actor* pPerkOwner; + uint8_t argumentCount; + uint8_t resultCount; +}; +static_assert(offsetof(HandleEntryPointVisitor, ppArguments) == 0x10); +static_assert(offsetof(HandleEntryPointVisitor, argumentCount) == 0x28); diff --git a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp index fe6a5ac5b8..0e98d08811 100644 --- a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp +++ b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -49,7 +50,18 @@ bool MagicTarget::AddTarget(AddTargetData& arData, bool aApplyHealPerkBonus, boo s_applyHealPerkBonus = aApplyHealPerkBonus; s_applyStaminaPerkBonus = aApplyStaminaPerkBonus; - bool result = TiltedPhoques::ThisCall(RealAddTarget, this, arData); + bool result = false; + if (arData.pSpell->IsHealingSpell() || arData.pSpell->IsBuffSpell()) + { + // The caster's perks are already part of what they send for these, see ApplyHealPerkBonus. + ScopedIgnoreRemotePerks ignoreRemotePerks; + result = TiltedPhoques::ThisCall(RealAddTarget, this, arData); + } + else + { + ScopedRemotePerks remotePerks; + result = TiltedPhoques::ThisCall(RealAddTarget, this, arData); + } s_autoSucceedEffectCheck = false; s_applyHealPerkBonus = false; @@ -153,6 +165,7 @@ bool TP_MAKE_THISCALL(HookAddTarget, MagicTarget, MagicTarget::AddTargetData& ar } } + ScopedRemotePerks remotePerks; bool result = TiltedPhoques::ThisCall(RealAddTarget, apThis, arData); if (result && arData.ShouldSync()) World::Get().GetRunner().Trigger(addTargetEvent); diff --git a/Code/client/Games/Skyrim/RemotePerks.cpp b/Code/client/Games/Skyrim/RemotePerks.cpp new file mode 100644 index 0000000000..93bd2d392d --- /dev/null +++ b/Code/client/Games/Skyrim/RemotePerks.cpp @@ -0,0 +1,228 @@ +#include + +#include +#include +#include + +#include + +#include +#include + +namespace +{ +TP_THIS_FUNCTION(TVisitPerks, void, Actor, PerkRankVisitor* apVisitor); +TP_THIS_FUNCTION(THasPerkEntries, bool, Actor, uint8_t aEntryPoint); +TP_THIS_FUNCTION(TForEachPerkEntry, void, Actor, uint8_t aEntryPoint, PerkEntryVisitor* apVisitor); + +TVisitPerks* RealVisitPerks = nullptr; +THasPerkEntries* RealHasPerkEntries = nullptr; +TForEachPerkEntry* RealForEachPerkEntry = nullptr; + +// Hits and spells can resolve on engine worker threads, so readers take a reference to an immutable list. +std::shared_mutex s_perksLock; +RemotePerks::PerkMap s_perks; + +thread_local uint32_t s_scopeDepth = 0; +thread_local uint32_t s_ignoreDepth = 0; + +RemotePerks::PerkDataPtr GetRemotePlayerPerks(Actor* apActor) noexcept +{ + if (s_ignoreDepth > 0 || !apActor->GetExtension()->IsRemotePlayer()) + return nullptr; + + if (!World::Get().GetServerSettings().PvpEnabled) + return nullptr; + + std::shared_lock lock(s_perksLock); + + const auto it = s_perks.find(apActor->formID); + return it != s_perks.end() ? it->second : nullptr; +} + +// Entry points on the attacker's side of a hit take the target as a filter argument. Everything else, such as the +// attacker's stamina costs or weapon speed, is simulated by the client that owns the attacker. +bool TargetsLocalPlayer(PerkEntryVisitor* apVisitor) noexcept +{ + POINTER_SKYRIMSE(uint8_t, s_handleEntryPointVisitorVtable, 195172); + + if (*reinterpret_cast(apVisitor) != s_handleEntryPointVisitorVtable.Get()) + return false; + + const auto* pVisitor = static_cast(apVisitor); + const void* pPlayer = PlayerCharacter::Get(); + + for (uint32_t i = 1; i < pVisitor->argumentCount; ++i) + { + if (pVisitor->ppArguments[i] == pPlayer) + return true; + } + + return false; +} + +void TP_MAKE_THISCALL(HookVisitPerks, Actor, PerkRankVisitor* apVisitor) +{ + if (s_scopeDepth > 0) + { + if (const auto pData = GetRemotePlayerPerks(apThis)) + { + for (const PerkRankData& cPerk : pData->Perks) + { + if (apVisitor->Visit(const_cast(&cPerk)) != 1) + break; + } + return; + } + } + + TiltedPhoques::ThisCall(RealVisitPerks, apThis, apVisitor); +} + +bool TP_MAKE_THISCALL(HookHasPerkEntries, Actor, uint8_t aEntryPoint) +{ + if (aEntryPoint < BGSEntryPointPerkEntry::kEntryPointCount) + { + if (const auto pData = GetRemotePlayerPerks(apThis)) + return !pData->Entries[aEntryPoint].empty(); + } + + return TiltedPhoques::ThisCall(RealHasPerkEntries, apThis, aEntryPoint); +} + +void TP_MAKE_THISCALL(HookForEachPerkEntry, Actor, uint8_t aEntryPoint, PerkEntryVisitor* apVisitor) +{ + if (aEntryPoint < BGSEntryPointPerkEntry::kEntryPointCount) + { + if (const auto pData = GetRemotePlayerPerks(apThis)) + { + if (TargetsLocalPlayer(apVisitor)) + { + // Perk conditions can check the attacker's perks + ScopedRemotePerks remotePerks; + + for (BGSPerkEntry* pEntry : pData->Entries[aEntryPoint]) + { + if (apVisitor->Visit(pEntry) != 1) + break; + } + } + return; + } + } + + TiltedPhoques::ThisCall(RealForEachPerkEntry, apThis, aEntryPoint, apVisitor); +} +} // namespace + +ScopedRemotePerks::ScopedRemotePerks() noexcept +{ + ++s_scopeDepth; +} + +ScopedRemotePerks::~ScopedRemotePerks() noexcept +{ + --s_scopeDepth; +} + +ScopedIgnoreRemotePerks::ScopedIgnoreRemotePerks() noexcept +{ + ++s_ignoreDepth; +} + +ScopedIgnoreRemotePerks::~ScopedIgnoreRemotePerks() noexcept +{ + --s_ignoreDepth; +} + +namespace RemotePerks +{ +PerkDataPtr BuildPerkData(const TiltedPhoques::Vector& acPerks) noexcept +{ + auto pData = std::make_shared(); + auto& modSystem = World::Get().GetModSystem(); + + for (const PlayerPerk& cPerk : acPerks) + { + const uint32_t cPerkId = modSystem.GetGameId(cPerk.Id); + BGSPerk* pPerk = cPerkId ? Cast(TESForm::GetById(cPerkId)) : nullptr; + if (!pPerk) + { + spdlog::debug("{}: skipping unknown perk, GameId base: {:X}, mod: {:X}", __FUNCTION__, cPerk.Id.BaseId, cPerk.Id.ModId); + continue; + } + + pData->Perks.push_back(PerkRankData{pPerk, cPerk.Rank}); + + for (BGSPerkEntry* pEntry : pPerk->perkEntries) + { + // BGSPerk::ApplyPerk only applies the entries of the rank the perk is at. + if (!pEntry || pEntry->rank + 1 != cPerk.Rank || pEntry->GetType() != BGSPerkEntry::kEntryPoint) + continue; + + const uint8_t cEntryPoint = static_cast(pEntry)->entryPoint; + if (cEntryPoint >= BGSEntryPointPerkEntry::kEntryPointCount) + continue; + + // Same order as AIProcess::AddPerkEntry: highest priority first, then lowest rank. + auto& entries = pData->Entries[cEntryPoint]; + const auto cPosition = std::upper_bound(entries.begin(), entries.end(), pEntry, [](const BGSPerkEntry* apLhs, const BGSPerkEntry* apRhs) { + if (apLhs->priority != apRhs->priority) + return apLhs->priority > apRhs->priority; + return apLhs->rank < apRhs->rank; + }); + entries.insert(cPosition, pEntry); + } + } + + return pData; +} + +void SetPerks(PerkMap&& aPerks) noexcept +{ + std::unique_lock lock(s_perksLock); + s_perks.swap(aPerks); +} + +void CollectLocalPlayerPerks(TiltedPhoques::Vector& aPerks) noexcept +{ + struct Visitor final : PerkRankVisitor + { + explicit Visitor(TiltedPhoques::Vector& aPerks) noexcept + : Perks(aPerks) + { + } + + uint32_t Visit(PerkRankData* apPerkRank) override + { + PlayerPerk perk{}; + if (apPerkRank && apPerkRank->pPerk && apPerkRank->rank > 0 && World::Get().GetModSystem().GetServerModId(apPerkRank->pPerk->formID, perk.Id)) + { + perk.Rank = apPerkRank->rank; + Perks.push_back(perk); + } + return 1; + } + + TiltedPhoques::Vector& Perks; + } visitor(aPerks); + + // PlayerCharacter::VisitPerks: the base form's perks, then the earned ones. + POINTER_SKYRIMSE(TVisitPerks, s_visitPlayerPerks, 40680); + TiltedPhoques::ThisCall(s_visitPlayerPerks.Get(), static_cast(PlayerCharacter::Get()), static_cast(&visitor)); +} +} // namespace RemotePerks + +static TiltedPhoques::Initializer s_remotePerkHooks([]() { + POINTER_SKYRIMSE(TVisitPerks, visitPerks, 37693); + POINTER_SKYRIMSE(THasPerkEntries, hasPerkEntries, 37701); + POINTER_SKYRIMSE(TForEachPerkEntry, forEachPerkEntry, 37702); + + RealVisitPerks = visitPerks.Get(); + RealHasPerkEntries = hasPerkEntries.Get(); + RealForEachPerkEntry = forEachPerkEntry.Get(); + + TP_HOOK(&RealVisitPerks, HookVisitPerks); + TP_HOOK(&RealHasPerkEntries, HookHasPerkEntries); + TP_HOOK(&RealForEachPerkEntry, HookForEachPerkEntry); +}); diff --git a/Code/client/Games/Skyrim/RemotePerks.h b/Code/client/Games/Skyrim/RemotePerks.h new file mode 100644 index 0000000000..2c655f60a9 --- /dev/null +++ b/Code/client/Games/Skyrim/RemotePerks.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include +#include + +/** + * Remote player copies have none of the perks their player earned: Character::VisitPerks only reads the TESNPC, those + * perks are stored on PlayerCharacter. When a remote player hits or casts on the local player, this client runs the + * attacker side of the hit against that copy, so the attacker's perks have to come from here. + * The perks are never added to the copy, since AddPerk would also run the perk's abilities on it. + */ +namespace RemotePerks +{ +struct PerkData +{ + std::vector Perks; + // Sorted the same way as AIProcess's perk entry arrays + std::vector Entries[BGSEntryPointPerkEntry::kEntryPointCount]; +}; + +using PerkDataPtr = std::shared_ptr; +using PerkMap = std::unordered_map; + +// Perks that aren't in the local player's load order are skipped. +PerkDataPtr BuildPerkData(const TiltedPhoques::Vector& acPerks) noexcept; +// Replaces the remote players' perks, keyed by the form id of their copy. +void SetPerks(PerkMap&& aPerks) noexcept; +// The local player's perks, those on the base form included. +void CollectLocalPlayerPerks(TiltedPhoques::Vector& aPerks) noexcept; +} // namespace RemotePerks + +// While alive, HasPerk and GetPerkRank on remote players answer from their perk list on this thread. +struct ScopedRemotePerks +{ + ScopedRemotePerks() noexcept; + ~ScopedRemotePerks() noexcept; + + TP_NOCOPYMOVE(ScopedRemotePerks); +}; + +// While alive, remote players' perk lists are ignored on this thread. +struct ScopedIgnoreRemotePerks +{ + ScopedIgnoreRemotePerks() noexcept; + ~ScopedIgnoreRemotePerks() noexcept; + + TP_NOCOPYMOVE(ScopedIgnoreRemotePerks); +}; diff --git a/Code/client/Services/Generic/PlayerService.cpp b/Code/client/Services/Generic/PlayerService.cpp index 6ced4d55b4..a4e31ab752 100644 --- a/Code/client/Services/Generic/PlayerService.cpp +++ b/Code/client/Services/Generic/PlayerService.cpp @@ -20,9 +20,15 @@ #include #include #include +#include +#include +#include +#include #include +#include + #include #include #include @@ -48,6 +54,9 @@ PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher, Trans m_playerLevelConnection = m_dispatcher.sink().connect<&PlayerService::OnPlayerLevelEvent>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&PlayerService::OnPartyJoinedEvent>(this); m_partyLeftConnection = aDispatcher.sink().connect<&PlayerService::OnPartyLeftEvent>(this); + m_notifyPlayerPerksConnection = aDispatcher.sink().connect<&PlayerService::OnNotifyPlayerPerks>(this); + m_playerJoinedConnection = aDispatcher.sink().connect<&PlayerService::OnNotifyPlayerJoined>(this); + m_playerLeftConnection = aDispatcher.sink().connect<&PlayerService::OnNotifyPlayerLeft>(this); } void PlayerService::OnUpdate(const UpdateEvent&) noexcept @@ -57,6 +66,7 @@ void PlayerService::OnUpdate(const UpdateEvent&) noexcept RunDifficultyUpdates(); RunLevelUpdates(); RunBeastFormDetection(); + RunPerkUpdates(); } void PlayerService::OnConnected(const ConnectedEvent& acEvent) noexcept @@ -85,6 +95,10 @@ void PlayerService::OnDisconnected(const DisconnectedEvent& acEvent) noexcept TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); pWorldEncountersEnabled->f = 1.f; + + m_arePerksSent = false; + m_remotePerks.clear(); + RemotePerks::SetPerks({}); } void PlayerService::OnServerSettingsReceived(const ServerSettings& acSettings) noexcept @@ -173,6 +187,22 @@ void PlayerService::OnPlayerLevelEvent(const PlayerLevelEvent& acEvent) const no m_transport.Send(request); } +void PlayerService::OnNotifyPlayerPerks(const NotifyPlayerPerks& acMessage) noexcept +{ + m_remotePerks[acMessage.PlayerId] = RemotePerks::BuildPerkData(acMessage.Perks); +} + +void PlayerService::OnNotifyPlayerJoined(const NotifyPlayerJoined& acMessage) noexcept +{ + // The server doesn't keep perk lists around, so the new player gets them from the other players. + m_arePerksSent = false; +} + +void PlayerService::OnNotifyPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept +{ + m_remotePerks.erase(acMessage.PlayerId); +} + void PlayerService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { // TODO: this can be done a bit prettier @@ -318,6 +348,55 @@ void PlayerService::RunLevelUpdates() const noexcept } } +void PlayerService::RunPerkUpdates() noexcept +{ + static std::chrono::steady_clock::time_point lastUpdateTimePoint; + constexpr auto cDelayBetweenUpdates = 1000ms; + + const auto now = std::chrono::steady_clock::now(); + if (now - lastUpdateTimePoint < cDelayBetweenUpdates) + return; + + lastUpdateTimePoint = now; + + if (!m_transport.IsConnected()) + return; + + if (!m_world.GetServerSettings().PvpEnabled) + { + // Perks are sent again if PvP gets enabled later on + m_arePerksSent = false; + m_remotePerks.clear(); + RemotePerks::SetPerks({}); + return; + } + + PlayerPerksRequest request{}; + RemotePerks::CollectLocalPlayerPerks(request.Perks); + + std::vector> perks; + perks.reserve(request.Perks.size()); + for (const PlayerPerk& cPerk : request.Perks) + perks.emplace_back(cPerk.Id, cPerk.Rank); + + if ((!m_arePerksSent || perks != m_sentPerks) && m_transport.Send(request)) + { + m_sentPerks = std::move(perks); + m_arePerksSent = true; + } + + // Copies get a new form id when they respawn, so look them up every time + RemotePerks::PerkMap formPerks; + auto view = m_world.view(); + for (const auto entity : view) + { + const auto it = m_remotePerks.find(view.get(entity).Id); + if (it != m_remotePerks.end()) + formPerks[view.get(entity).Id] = it->second; + } + RemotePerks::SetPerks(std::move(formPerks)); +} + void PlayerService::RunBeastFormDetection() const noexcept { static uint32_t lastRaceFormID = 0; diff --git a/Code/client/Services/PlayerService.h b/Code/client/Services/PlayerService.h index 8ed51a823d..330d62f227 100644 --- a/Code/client/Services/PlayerService.h +++ b/Code/client/Services/PlayerService.h @@ -1,5 +1,7 @@ #pragma once +#include + struct World; struct TransportService; @@ -15,6 +17,9 @@ struct PartyJoinedEvent; struct PartyLeftEvent; struct NotifyPlayerRespawn; +struct NotifyPlayerPerks; +struct NotifyPlayerJoined; +struct NotifyPlayerLeft; /** * @brief Handles logic related to the local player. @@ -38,6 +43,9 @@ struct PlayerService void OnPlayerLevelEvent(const PlayerLevelEvent& acEvent) const noexcept; void OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept; void OnPartyLeftEvent(const PartyLeftEvent& acEvent) noexcept; + void OnNotifyPlayerPerks(const NotifyPlayerPerks& acMessage) noexcept; + void OnNotifyPlayerJoined(const NotifyPlayerJoined& acMessage) noexcept; + void OnNotifyPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept; private: /** @@ -51,6 +59,10 @@ struct PlayerService void RunDifficultyUpdates() const noexcept; void RunLevelUpdates() const noexcept; void RunBeastFormDetection() const noexcept; + /** + * @brief Share the local player's perks when they change, and tie the remote players' perks to their copies. + */ + void RunPerkUpdates() noexcept; void ToggleDeathSystem(bool aSet) noexcept; @@ -74,6 +86,10 @@ struct PlayerService uint32_t m_cachedSecondarySpellId = 0; uint32_t m_cachedPowerId = 0; + std::vector> m_sentPerks; + bool m_arePerksSent = false; + std::unordered_map m_remotePerks; + entt::scoped_connection m_updateConnection; entt::scoped_connection m_connectedConnection; entt::scoped_connection m_disconnectedConnection; @@ -85,4 +101,7 @@ struct PlayerService entt::scoped_connection m_playerLevelConnection; entt::scoped_connection m_partyJoinedConnection; entt::scoped_connection m_partyLeftConnection; + entt::scoped_connection m_notifyPlayerPerksConnection; + entt::scoped_connection m_playerJoinedConnection; + entt::scoped_connection m_playerLeftConnection; }; diff --git a/Code/encoding/Messages/ClientMessageFactory.h b/Code/encoding/Messages/ClientMessageFactory.h index 4f94d6d10e..65ab4267bc 100644 --- a/Code/encoding/Messages/ClientMessageFactory.h +++ b/Code/encoding/Messages/ClientMessageFactory.h @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -70,7 +71,7 @@ struct ClientMessageFactory 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, RequestActionEvents>; + RequestRemoveWaypoint, RemoveSpellRequest, SetTimeCommandRequest, RequestActionEvents, PlayerPerksRequest>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/NotifyPlayerPerks.cpp b/Code/encoding/Messages/NotifyPlayerPerks.cpp new file mode 100644 index 0000000000..f9b3a2ebfb --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerPerks.cpp @@ -0,0 +1,23 @@ +#include + +#include + +void NotifyPlayerPerks::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PlayerId); + Serialization::WriteVarInt(aWriter, Perks.size()); + + for (const auto& perk : Perks) + perk.Serialize(aWriter); +} + +void NotifyPlayerPerks::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Perks.resize(Serialization::ReadVarInt(aReader)); + + for (auto& perk : Perks) + perk.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyPlayerPerks.h b/Code/encoding/Messages/NotifyPlayerPerks.h new file mode 100644 index 0000000000..877e2f5a59 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerPerks.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Message.h" + +#include + +using TiltedPhoques::Vector; + +struct NotifyPlayerPerks final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPlayerPerks; + + NotifyPlayerPerks() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPlayerPerks& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && Perks == acRhs.Perks; } + + uint32_t PlayerId{}; + Vector Perks{}; +}; diff --git a/Code/encoding/Messages/PlayerPerksRequest.cpp b/Code/encoding/Messages/PlayerPerksRequest.cpp new file mode 100644 index 0000000000..a1bea97f30 --- /dev/null +++ b/Code/encoding/Messages/PlayerPerksRequest.cpp @@ -0,0 +1,21 @@ +#include + +#include + +void PlayerPerksRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, Perks.size()); + + for (const auto& perk : Perks) + perk.Serialize(aWriter); +} + +void PlayerPerksRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + Perks.resize(Serialization::ReadVarInt(aReader)); + + for (auto& perk : Perks) + perk.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/PlayerPerksRequest.h b/Code/encoding/Messages/PlayerPerksRequest.h new file mode 100644 index 0000000000..8e342ff2c3 --- /dev/null +++ b/Code/encoding/Messages/PlayerPerksRequest.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Message.h" + +#include + +using TiltedPhoques::Vector; + +struct PlayerPerksRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPlayerPerksRequest; + + PlayerPerksRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const PlayerPerksRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && Perks == acRhs.Perks; } + + Vector Perks{}; +}; diff --git a/Code/encoding/Messages/ServerMessageFactory.h b/Code/encoding/Messages/ServerMessageFactory.h index 516878b1d9..3d5d8356d4 100644 --- a/Code/encoding/Messages/ServerMessageFactory.h +++ b/Code/encoding/Messages/ServerMessageFactory.h @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -73,7 +74,7 @@ struct ServerMessageFactory NotifyActorValueChanges, NotifyPartyJoined, NotifyPartyLeft, NotifyActorMaxValueChanges, NotifyHealthChangeBroadcast, NotifyActivate, NotifyLockChange, AssignObjectsResponse, NotifyDeathStateChange, NotifyOwnershipTransfer, NotifyObjectInventoryChanges, NotifySpellCast, NotifyProjectileLaunch, NotifyInterruptCast, NotifyAddTarget, NotifyScriptAnimation, NotifyDrawWeapon, NotifyMount, NotifyNewPackage, NotifyRespawn, NotifySyncExperience, NotifyEquipmentChanges, NotifyChatMessageBroadcast, TeleportCommandResponse, NotifyPlayerRespawn, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyPlayerLeft, NotifyPlayerJoined, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, - NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult, NotifyRemoveSpell, NotifyActionEvents>; + NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult, NotifyRemoveSpell, NotifyActionEvents, NotifyPlayerPerks>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Opcodes.h b/Code/encoding/Opcodes.h index baab493ccb..881da737f4 100644 --- a/Code/encoding/Opcodes.h +++ b/Code/encoding/Opcodes.h @@ -54,6 +54,7 @@ enum ClientOpcode : unsigned char kRequestRemoveWaypoint, kSetTimeCommandRequest, kRequestActionEvents, + kPlayerPerksRequest, kClientOpcodeMax }; @@ -114,5 +115,6 @@ enum ServerOpcode : unsigned char kNotifyRemoveWaypoint, kNotifySetTimeResult, kNotifyActionEvents, + kNotifyPlayerPerks, kServerOpcodeMax }; diff --git a/Code/encoding/Structs/PlayerPerk.cpp b/Code/encoding/Structs/PlayerPerk.cpp new file mode 100644 index 0000000000..abed517a2a --- /dev/null +++ b/Code/encoding/Structs/PlayerPerk.cpp @@ -0,0 +1,20 @@ +#include + +#include + +using TiltedPhoques::Serialization; + +void PlayerPerk::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Id.Serialize(aWriter); + aWriter.WriteBits(Rank, 8); +} + +void PlayerPerk::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + Id.Deserialize(aReader); + + uint64_t rank = 0; + aReader.ReadBits(rank, 8); + Rank = rank & 0xFF; +} diff --git a/Code/encoding/Structs/PlayerPerk.h b/Code/encoding/Structs/PlayerPerk.h new file mode 100644 index 0000000000..e8d418e435 --- /dev/null +++ b/Code/encoding/Structs/PlayerPerk.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +/** + * @brief A perk a player has, and the rank they have it at. + */ +struct PlayerPerk +{ + bool operator==(const PlayerPerk& acRhs) const noexcept { return Id == acRhs.Id && Rank == acRhs.Rank; } + bool operator!=(const PlayerPerk& acRhs) const noexcept { return !operator==(acRhs); } + + void Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; + void Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept; + + GameId Id{}; + uint8_t Rank{}; +}; diff --git a/Code/server/Services/PlayerService.cpp b/Code/server/Services/PlayerService.cpp index e887a6ae28..a020ecab99 100644 --- a/Code/server/Services/PlayerService.cpp +++ b/Code/server/Services/PlayerService.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -31,6 +33,7 @@ PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher) noexc , m_exteriorCellEnterConnection(aDispatcher.sink>().connect<&PlayerService::HandleExteriorCellEnter>(this)) , m_playerRespawnConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerRespawnRequest>(this)) , m_playerLevelConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerLevelRequest>(this)) + , m_playerPerksConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerPerksRequest>(this)) { } @@ -215,3 +218,12 @@ void PlayerService::OnPlayerLevelRequest(const PacketEvent& GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); } + +void PlayerService::OnPlayerPerksRequest(const PacketEvent& acMessage) const noexcept +{ + NotifyPlayerPerks notify{}; + notify.PlayerId = acMessage.pPlayer->GetId(); + notify.Perks = acMessage.Packet.Perks; + + GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); +} diff --git a/Code/server/Services/PlayerService.h b/Code/server/Services/PlayerService.h index 9ac7351762..ba4c179267 100644 --- a/Code/server/Services/PlayerService.h +++ b/Code/server/Services/PlayerService.h @@ -8,6 +8,7 @@ struct EnterInteriorCellRequest; struct EnterExteriorCellRequest; struct PlayerRespawnRequest; struct PlayerLevelRequest; +struct PlayerPerksRequest; /** * @brief Handles player specific actions that might change the information needed by other clients about that player. @@ -25,6 +26,7 @@ struct PlayerService void HandleInteriorCellEnter(const PacketEvent& acMessage) const noexcept; void OnPlayerRespawnRequest(const PacketEvent& acMessage) const noexcept; void OnPlayerLevelRequest(const PacketEvent& acMessage) const noexcept; + void OnPlayerPerksRequest(const PacketEvent& acMessage) const noexcept; private: World& m_world; @@ -34,4 +36,5 @@ struct PlayerService entt::scoped_connection m_interiorCellEnterConnection; entt::scoped_connection m_playerRespawnConnection; entt::scoped_connection m_playerLevelConnection; + entt::scoped_connection m_playerPerksConnection; }; diff --git a/Code/tests/encoding.cpp b/Code/tests/encoding.cpp index defb808418..248fd672bf 100644 --- a/Code/tests/encoding.cpp +++ b/Code/tests/encoding.cpp @@ -63,6 +63,47 @@ TEST_CASE("Encoding factory", "[encoding.factory]") auto pRequest = CastUnique(std::move(pMessage)); REQUIRE(pRequest->InviterId == request.InviterId); } + + { + PlayerPerksRequest request; + request.Perks.push_back(PlayerPerk{GameId(1, 0x581f8), 1}); + request.Perks.push_back(PlayerPerk{GameId(0xFE012, 0x805), 3}); + + Buffer::Writer writer(&buff); + request.Serialize(writer); + + Buffer::Reader reader(&buff); + + const ClientMessageFactory factory; + auto pMessage = factory.Extract(reader); + + REQUIRE(pMessage); + REQUIRE(pMessage->GetOpcode() == request.GetOpcode()); + + auto pRequest = CastUnique(std::move(pMessage)); + REQUIRE(pRequest->Perks == request.Perks); + } + + { + NotifyPlayerPerks notify; + notify.PlayerId = 1234; + notify.Perks.push_back(PlayerPerk{GameId(1, 0x581f9), 2}); + + Buffer::Writer writer(&buff); + notify.Serialize(writer); + + Buffer::Reader reader(&buff); + + const ServerMessageFactory factory; + auto pMessage = factory.Extract(reader); + + REQUIRE(pMessage); + REQUIRE(pMessage->GetOpcode() == notify.GetOpcode()); + + auto pNotify = CastUnique(std::move(pMessage)); + REQUIRE(pNotify->PlayerId == notify.PlayerId); + REQUIRE(pNotify->Perks == notify.Perks); + } } TEST_CASE("Static structures", "[encoding.static]")