diff --git a/Code/client/Events/BeastFormChangeEvent.h b/Code/client/Events/BeastFormChangeEvent.h new file mode 100644 index 000000000..cc7576207 --- /dev/null +++ b/Code/client/Events/BeastFormChangeEvent.h @@ -0,0 +1,8 @@ +#pragma once + +/** + * @brief Dispatched when the local player enters or leaves beast form (vampire lord, werewolf). + */ +struct BeastFormChangeEvent +{ +}; diff --git a/Code/client/Events/LeaveBeastFormEvent.h b/Code/client/Events/LeaveBeastFormEvent.h deleted file mode 100644 index 211dac5b3..000000000 --- a/Code/client/Events/LeaveBeastFormEvent.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -/** - * @brief Dispatched when the local player leaves beast form (vampire lord, werewolf). - */ -struct LeaveBeastFormEvent -{ -}; diff --git a/Code/client/Games/Animation.cpp b/Code/client/Games/Animation.cpp index 557e44b3c..0c243f436 100644 --- a/Code/client/Games/Animation.cpp +++ b/Code/client/Games/Animation.cpp @@ -17,6 +17,7 @@ TP_THIS_FUNCTION(TPerformAction, uint8_t, ActorMediator, TESActionData* apAction); static TPerformAction* RealPerformAction; +// TODO: make scoped override thread_local bool g_forceAnimation = false; uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction) diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index ad6207a01..f587dcab1 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -21,6 +21,7 @@ #include #include +#include #include @@ -579,12 +580,58 @@ void Actor::Reset() noexcept s_pReset(this, 0, nullptr); } +bool Actor::PlayIdle(TESIdleForm* apIdle) noexcept +{ + PAPYRUS_FUNCTION(bool, Actor, PlayIdle, TESIdleForm*); + return s_pPlayIdle(this, apIdle); +} + void Actor::Respawn() noexcept { Resurrect(false); Reset(); } +bool Actor::IsVampireLord() const noexcept +{ + return race && race->formID == 0x200283A; +} + +extern thread_local bool g_forceAnimation; + +void Actor::FixVampireLordModel() noexcept +{ + TESBoundObject* pObject = Cast(TESForm::GetById(0x2011a84)); + if (!pObject) + return; + + { + ScopedInventoryOverride _; + AddObjectToContainer(pObject, nullptr, 1, nullptr); + } + + EquipManager::Get()->Equip(this, pObject, nullptr, 1, nullptr, false, true, false, false); + + g_forceAnimation = true; + + BSFixedString str("isLevitating"); + uint32_t isLevitating = GetAnimationVariableInt(&str); + spdlog::critical("isLevitating {}", isLevitating); + + // By default, a loaded vampire lord is not levitating. + if (isLevitating) + { + BSFixedString levitation("LevitationToggle"); + SendAnimationEvent(&levitation); + } + + // TODO: weapon draw code does not seem to take care of this + //BSFixedString weapEquip("WeapEquip"); + //SendAnimationEvent(&weapEquip); + + g_forceAnimation = false; +} + TP_THIS_FUNCTION(TForceState, void, Actor, const NiPoint3&, float, float, TESObjectCELL*, TESWorldSpace*, bool); static TForceState* RealForceState = nullptr; diff --git a/Code/client/Games/Skyrim/Actor.h b/Code/client/Games/Skyrim/Actor.h index 8ef8e5459..ba9effc70 100644 --- a/Code/client/Games/Skyrim/Actor.h +++ b/Code/client/Games/Skyrim/Actor.h @@ -21,6 +21,7 @@ struct ExPlayerCharacter; struct ActorExtension; struct AIProcess; struct CombatController; +struct TESIdleForm; struct Actor : TESObjectREFR { @@ -208,6 +209,7 @@ struct Actor : TESObjectREFR [[nodiscard]] uint8_t GetPerkRank(uint32_t aPerkFormId) const noexcept; [[nodiscard]] bool IsWearingBodyPiece() const noexcept; [[nodiscard]] bool ShouldWearBodyPiece() const noexcept; + [[nodiscard]] bool IsVampireLord() const noexcept; // Setters void SetSpeed(float aSpeed) noexcept; @@ -246,6 +248,8 @@ struct Actor : TESObjectREFR void SetCombatTargetEx(Actor* apTarget) noexcept; void StartCombat(Actor* apTarget) noexcept; void StopCombat() noexcept; + bool PlayIdle(TESIdleForm* apIdle) noexcept; + void FixVampireLordModel() noexcept; enum ActorFlags { diff --git a/Code/client/Games/Skyrim/PlayerCharacter.cpp b/Code/client/Games/Skyrim/PlayerCharacter.cpp index 650276f7e..9800851ef 100644 --- a/Code/client/Games/Skyrim/PlayerCharacter.cpp +++ b/Code/client/Games/Skyrim/PlayerCharacter.cpp @@ -2,11 +2,12 @@ #include #include +#include #include #include -#include +#include #include #include #include @@ -162,7 +163,7 @@ void TP_MAKE_THISCALL(HookSetBeastForm, void, void* apUnk1, void* apUnk2, bool a if (!aEntering) { PlayerCharacter::Get()->GetExtension()->GraphDescriptorHash = AnimationGraphDescriptor_Master_Behavior::m_key; - World::Get().GetRunner().Trigger(LeaveBeastFormEvent()); + World::Get().GetRunner().Trigger(BeastFormChangeEvent()); } TiltedPhoques::ThisCall(RealSetBeastForm, apThis, apUnk1, apUnk2, aEntering); diff --git a/Code/client/Games/Skyrim/TESObjectREFR.cpp b/Code/client/Games/Skyrim/TESObjectREFR.cpp index a1e93bcfa..eb56bd92f 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.cpp +++ b/Code/client/Games/Skyrim/TESObjectREFR.cpp @@ -536,6 +536,15 @@ void TESObjectREFR::EnableImpl() noexcept TiltedPhoques::ThisCall(s_enable, this, false); } +uint32_t TESObjectREFR::GetAnimationVariableInt(BSFixedString* apVariableName) noexcept +{ + using ObjectReference = TESObjectREFR; + + PAPYRUS_FUNCTION(uint32_t, ObjectReference, GetAnimationVariableInt, BSFixedString*); + + return s_pGetAnimationVariableInt(this, apVariableName); +} + static thread_local bool s_cancelAnimationWaitEvent = false; bool TESObjectREFR::PlayAnimationAndWait(BSFixedString* apAnimation, BSFixedString* apEventName) noexcept @@ -578,6 +587,11 @@ bool TESObjectREFR::PlayAnimation(BSFixedString* apEventName) noexcept return result; } +bool TESObjectREFR::SendAnimationEvent(BSFixedString* apEventName) noexcept +{ + return animationGraphHolder.SendAnimationEvent(apEventName); +} + bool TP_MAKE_THISCALL(HookPlayAnimation, void, uint32_t auiStackID, TESObjectREFR* apSelf, BSFixedString* apEventName) { spdlog::debug("EventName: {}", apEventName->AsAscii()); diff --git a/Code/client/Games/Skyrim/TESObjectREFR.h b/Code/client/Games/Skyrim/TESObjectREFR.h index 6e1d856d6..d6d2aec91 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.h +++ b/Code/client/Games/Skyrim/TESObjectREFR.h @@ -167,6 +167,7 @@ struct TESObjectREFR : TESForm void SaveAnimationVariables(AnimationVariables& aWriter) const noexcept; void LoadAnimationVariables(const AnimationVariables& aReader) const noexcept; + uint32_t GetAnimationVariableInt(BSFixedString* apVariableName) noexcept; void RemoveAllItems() noexcept; void Delete() const noexcept; @@ -175,6 +176,7 @@ struct TESObjectREFR : TESForm void MoveTo(TESObjectCELL* apCell, const NiPoint3& acPosition) const noexcept; void PayGold(int32_t aAmount) noexcept; void PayGoldToContainer(TESObjectREFR* pContainer, int32_t aAmount) noexcept; + bool SendAnimationEvent(BSFixedString* apEventName) noexcept; bool Activate(TESObjectREFR* apActivator, uint8_t aUnk1, TESBoundObject* apObjectToGet, int32_t aCount, char aDefaultProcessing) noexcept; diff --git a/Code/client/Services/CharacterService.h b/Code/client/Services/CharacterService.h index 8f098a701..7ae13a0e6 100644 --- a/Code/client/Services/CharacterService.h +++ b/Code/client/Services/CharacterService.h @@ -31,7 +31,7 @@ struct NotifyMount; struct InitPackageEvent; struct NotifyNewPackage; struct NotifyRespawn; -struct LeaveBeastFormEvent; +struct BeastFormChangeEvent; struct AddExperienceEvent; struct NotifySyncExperience; struct DialogueEvent; @@ -78,7 +78,7 @@ struct CharacterService void OnInitPackageEvent(const InitPackageEvent& acEvent) const noexcept; void OnNotifyNewPackage(const NotifyNewPackage& acMessage) const noexcept; void OnNotifyRespawn(const NotifyRespawn& acMessage) const noexcept; - void OnLeaveBeastForm(const LeaveBeastFormEvent& acEvent) const noexcept; + void OnBeastFormChange(const BeastFormChangeEvent& acEvent) const noexcept; void OnAddExperienceEvent(const AddExperienceEvent& acEvent) noexcept; void OnNotifySyncExperience(const NotifySyncExperience& acMessage) noexcept; void OnDialogueEvent(const DialogueEvent& acEvent) noexcept; @@ -114,6 +114,7 @@ struct CharacterService float m_cachedExperience = 0.f; + // TODO: revamp this, read the local anim var like vampire lord? struct WeaponDrawData { WeaponDrawData() = default; @@ -147,7 +148,7 @@ struct CharacterService entt::scoped_connection m_initPackageConnection; entt::scoped_connection m_newPackageConnection; entt::scoped_connection m_notifyRespawnConnection; - entt::scoped_connection m_leaveBeastFormConnection; + entt::scoped_connection m_beastFormChangeConnection; entt::scoped_connection m_addExperienceEventConnection; entt::scoped_connection m_syncExperienceConnection; entt::scoped_connection m_dialogueEventConnection; diff --git a/Code/client/Services/Debug/DebugService.cpp b/Code/client/Services/Debug/DebugService.cpp index 1bb8fd4f5..00c6ef9cb 100644 --- a/Code/client/Services/Debug/DebugService.cpp +++ b/Code/client/Services/Debug/DebugService.cpp @@ -147,6 +147,8 @@ void DebugService::OnMoveActor(const MoveActorEvent& acEvent) noexcept moveData.position = acEvent.Position; } +extern thread_local bool g_forceAnimation; + void DebugService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { if (!BSGraphics::GetMainWindow()->IsForeground()) @@ -197,11 +199,7 @@ void DebugService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { s_f7Pressed = true; - static char s_address[256] = "de.playtogether.gg:10100"; - if (!m_transport.IsOnline()) - m_transport.Connect(s_address); - else - m_transport.Close(); + // } } else @@ -213,7 +211,7 @@ void DebugService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { s_f8Pressed = true; - PlaceActorInWorld(); + //PlaceActorInWorld(); } } else diff --git a/Code/client/Services/Generic/CharacterService.cpp b/Code/client/Services/Generic/CharacterService.cpp index c43734949..0bb9f705f 100644 --- a/Code/client/Services/Generic/CharacterService.cpp +++ b/Code/client/Services/Generic/CharacterService.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include #include #include @@ -95,7 +95,7 @@ CharacterService::CharacterService(World& aWorld, entt::dispatcher& aDispatcher, m_newPackageConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyNewPackage>(this); m_notifyRespawnConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyRespawn>(this); - m_leaveBeastFormConnection = m_dispatcher.sink().connect<&CharacterService::OnLeaveBeastForm>(this); + m_beastFormChangeConnection = m_dispatcher.sink().connect<&CharacterService::OnBeastFormChange>(this); m_addExperienceEventConnection = m_dispatcher.sink().connect<&CharacterService::OnAddExperienceEvent>(this); m_syncExperienceConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifySyncExperience>(this); @@ -404,7 +404,7 @@ void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage) if (waitingItor != std::end(waitingView)) { - spdlog::info("Character with form id {:X} already has a spawn request in progress.", acMessage.FormId); + spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId); return; } @@ -634,23 +634,33 @@ void CharacterService::OnRemoveCharacter(const NotifyRemoveCharacter& acMessage) void CharacterService::OnNotifyRespawn(const NotifyRespawn& acMessage) const noexcept { - Actor* pActor = Utils::GetByServerId(acMessage.ActorId); - if (!pActor) + auto view = m_world.view(); + const auto entityIt = std::find_if(view.begin(), view.end(), [view, id = acMessage.ActorId](auto aEntity) { return view.get(aEntity).Id == id; }); + + if (entityIt == view.end()) { - spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ActorId); + spdlog::error("Actor to respawn not found in: {:X}", acMessage.ActorId); return; } - pActor->Delete(); + const auto cId = *entityIt; + + auto& formIdComponent = view.get(cId); + CancelServerAssignment(*entityIt, formIdComponent.Id); + + if (m_world.all_of(cId)) + m_world.remove(cId); - // TODO: delete components? + if (m_world.orphan(cId)) + m_world.destroy(cId); RequestRespawn request; request.ActorId = acMessage.ActorId; + m_transport.Send(request); } -void CharacterService::OnLeaveBeastForm(const LeaveBeastFormEvent& acEvent) const noexcept +void CharacterService::OnBeastFormChange(const BeastFormChangeEvent& acEvent) const noexcept { auto view = m_world.view(); @@ -669,8 +679,15 @@ void CharacterService::OnLeaveBeastForm(const LeaveBeastFormEvent& acEvent) cons request.ActorId = serverId; Actor* pActor = Utils::GetByServerId(serverId); - if (pActor) - pActor->Delete(); + if (!pActor) + return; + + TESNPC* pNpc = Cast(pActor->baseForm); + if (!pNpc) + return; + + pNpc->Serialize(&request.AppearanceBuffer); + request.ChangeFlags = pNpc->GetChangeFlags(); m_transport.Send(request); } @@ -1512,6 +1529,11 @@ void CharacterService::RunRemoteUpdates() noexcept if (pActor->IsDead() != waitingFor3D.SpawnRequest.IsDead) waitingFor3D.SpawnRequest.IsDead ? pActor->Kill() : pActor->Respawn(); +#if TP_SKYRIM64 + if (pActor->IsVampireLord()) + pActor->FixVampireLordModel(); +#endif + toRemove.push_back(entity); spdlog::info("Applied 3D for actor, form id: {:X}", pActor->formID); diff --git a/Code/client/Services/Generic/InventoryService.cpp b/Code/client/Services/Generic/InventoryService.cpp index 02dc51639..fbf71b01e 100644 --- a/Code/client/Services/Generic/InventoryService.cpp +++ b/Code/client/Services/Generic/InventoryService.cpp @@ -121,6 +121,8 @@ void InventoryService::OnEquipmentChangeEvent(const EquipmentChangeEvent& acEven request.CurrentInventory = pActor->GetEquipment(); m_transport.Send(request); + + spdlog::info("Sending equipment request, item: {:X}, count: {}, target object: {:X}", acEvent.ItemId, acEvent.Count, acEvent.ActorId); } void InventoryService::OnNotifyInventoryChanges(const NotifyInventoryChanges& acMessage) noexcept diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index 26eefb476..3f1f500a0 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include diff --git a/Code/client/Services/Generic/PlayerService.cpp b/Code/client/Services/Generic/PlayerService.cpp index 9e11138b8..bf4def43b 100644 --- a/Code/client/Services/Generic/PlayerService.cpp +++ b/Code/client/Services/Generic/PlayerService.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) @@ -54,6 +56,7 @@ void PlayerService::OnUpdate(const UpdateEvent& acEvent) noexcept RunPostDeathUpdates(acEvent.Delta); RunDifficultyUpdates(); RunLevelUpdates(); + RunBeastFormDetection(); } void PlayerService::OnConnected(const ConnectedEvent& acEvent) noexcept @@ -354,6 +357,33 @@ void PlayerService::RunLevelUpdates() const noexcept } } +void PlayerService::RunBeastFormDetection() const noexcept +{ +#if TP_SKYRIM64 + static uint32_t lastRaceFormID = 0; + static std::chrono::steady_clock::time_point lastSendTimePoint; + constexpr auto cDelayBetweenUpdates = 250ms; + + const auto now = std::chrono::steady_clock::now(); + if (now - lastSendTimePoint < cDelayBetweenUpdates) + return; + + lastSendTimePoint = now; + + PlayerCharacter* pPlayer = PlayerCharacter::Get(); + if (!pPlayer->race) + return; + + if (pPlayer->race->formID == lastRaceFormID) + return; + + if (pPlayer->race->formID == 0x200283A || pPlayer->race->formID == 0xCDD84) + m_world.GetDispatcher().trigger(BeastFormChangeEvent()); + + lastRaceFormID = pPlayer->race->formID; +#endif +} + void PlayerService::ToggleDeathSystem(bool aSet) noexcept { m_isDeathSystemEnabled = aSet; diff --git a/Code/client/Services/PlayerService.h b/Code/client/Services/PlayerService.h index add0488c3..468a051cc 100644 --- a/Code/client/Services/PlayerService.h +++ b/Code/client/Services/PlayerService.h @@ -50,6 +50,7 @@ struct PlayerService */ void RunDifficultyUpdates() const noexcept; void RunLevelUpdates() const noexcept; + void RunBeastFormDetection() const noexcept; void ToggleDeathSystem(bool aSet) noexcept; diff --git a/Code/encoding/Messages/RequestRespawn.cpp b/Code/encoding/Messages/RequestRespawn.cpp index 19b956fc9..052f86d79 100644 --- a/Code/encoding/Messages/RequestRespawn.cpp +++ b/Code/encoding/Messages/RequestRespawn.cpp @@ -3,6 +3,8 @@ void RequestRespawn::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept { Serialization::WriteVarInt(aWriter, ActorId); + Serialization::WriteString(aWriter, AppearanceBuffer); + Serialization::WriteVarInt(aWriter, ChangeFlags); } void RequestRespawn::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -10,4 +12,6 @@ void RequestRespawn::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noex ClientMessage::DeserializeRaw(aReader); ActorId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + AppearanceBuffer = Serialization::ReadString(aReader); + ChangeFlags = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; } diff --git a/Code/encoding/Messages/RequestRespawn.h b/Code/encoding/Messages/RequestRespawn.h index 50cf7d81c..877aec2f3 100644 --- a/Code/encoding/Messages/RequestRespawn.h +++ b/Code/encoding/Messages/RequestRespawn.h @@ -14,7 +14,9 @@ struct RequestRespawn final : ClientMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const RequestRespawn& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ActorId == acRhs.ActorId; } + bool operator==(const RequestRespawn& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ActorId == acRhs.ActorId && AppearanceBuffer == acRhs.AppearanceBuffer && ChangeFlags == acRhs.ChangeFlags; } - uint32_t ActorId; + uint32_t ActorId{}; + String AppearanceBuffer{}; + uint32_t ChangeFlags{0}; }; diff --git a/Code/server/Services/CharacterService.cpp b/Code/server/Services/CharacterService.cpp index f54c031eb..690dae088 100644 --- a/Code/server/Services/CharacterService.cpp +++ b/Code/server/Services/CharacterService.cpp @@ -480,7 +480,7 @@ void CharacterService::OnNewPackageRequest(const PacketEvent& void CharacterService::OnRequestRespawn(const PacketEvent& acMessage) const noexcept { - auto view = m_world.view(); + auto view = m_world.view(); auto it = view.find(static_cast(acMessage.Packet.ActorId)); if (it == view.end()) { @@ -491,6 +491,13 @@ void CharacterService::OnRequestRespawn(const PacketEvent& acMes auto& ownerComponent = view.get(*it); if (ownerComponent.GetOwner() == acMessage.pPlayer) { + if (!acMessage.Packet.AppearanceBuffer.empty()) + { + auto& characterComponent = view.get(*it); + characterComponent.SaveBuffer = acMessage.Packet.AppearanceBuffer; + characterComponent.ChangeFlags = acMessage.Packet.ChangeFlags; + } + NotifyRespawn notify; notify.ActorId = acMessage.Packet.ActorId;