#include "Forms/TESObjectCELL.h" #include "Forms/TESWorldSpace.h" #include "Services/PapyrusService.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include CharacterService::CharacterService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) , m_dispatcher(aDispatcher) , m_transport(aTransport) { m_referenceAddedConnection = m_dispatcher.sink().connect<&CharacterService::OnActorAdded>(this); m_referenceRemovedConnection = m_dispatcher.sink().connect<&CharacterService::OnActorRemoved>(this); m_updateConnection = m_dispatcher.sink().connect<&CharacterService::OnUpdate>(this); m_actionConnection = m_dispatcher.sink().connect<&CharacterService::OnActionEvent>(this); m_connectedConnection = m_dispatcher.sink().connect<&CharacterService::OnConnected>(this); m_disconnectedConnection = m_dispatcher.sink().connect<&CharacterService::OnDisconnected>(this); m_assignCharacterConnection = m_dispatcher.sink().connect<&CharacterService::OnAssignCharacter>(this); m_characterSpawnConnection = m_dispatcher.sink().connect<&CharacterService::OnCharacterSpawn>(this); m_referenceMovementSnapshotConnection = m_dispatcher.sink().connect<&CharacterService::OnReferencesMoveRequest>(this); m_factionsConnection = m_dispatcher.sink().connect<&CharacterService::OnFactionsChanges>(this); m_ownershipTransferConnection = m_dispatcher.sink().connect<&CharacterService::OnOwnershipTransfer>(this); m_removeCharacterConnection = m_dispatcher.sink().connect<&CharacterService::OnRemoveCharacter>(this); m_remoteSpawnDataReceivedConnection = m_dispatcher.sink().connect<&CharacterService::OnRemoteSpawnDataReceived>(this); m_mountConnection = m_dispatcher.sink().connect<&CharacterService::OnMountEvent>(this); m_notifyMountConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyMount>(this); m_initPackageConnection = m_dispatcher.sink().connect<&CharacterService::OnInitPackageEvent>(this); m_newPackageConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyNewPackage>(this); m_notifyRespawnConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyRespawn>(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); m_dialogueEventConnection = m_dispatcher.sink().connect<&CharacterService::OnDialogueEvent>(this); m_dialogueSyncConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyDialogue>(this); m_subtitleEventConnection = m_dispatcher.sink().connect<&CharacterService::OnSubtitleEvent>(this); m_subtitleSyncConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifySubtitle>(this); m_actorTeleportConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyActorTeleport>(this); m_relinquishConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyRelinquishControl>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&CharacterService::OnPartyJoinedEvent>(this); } void CharacterService::DeleteRemoteEntityComponents(entt::entity aEntity) const noexcept { m_world.remove(aEntity); } bool CharacterService::TakeOwnership(const uint32_t acFormId, const uint32_t acServerId, const entt::entity acEntity) const noexcept { Actor* pActor = Cast(TESForm::GetById(acFormId)); if (!pActor) { spdlog::error("Cannot find actor to take control over, form id: {:X}, server id: {:X}", acFormId, acServerId); return false; } ActorExtension* pExtension = pActor->GetExtension(); if (pExtension->IsRemotePlayer()) { spdlog::error("Cannot take control over remote player actor, form id: {:X}, server id: {:X}", acFormId, acServerId); return false; } if (pActor->IsPlayerSummon()) { spdlog::error("Cannot take control over remote player summon, form id: {:X}, server id: {:X}", acFormId, acServerId); return false; } pExtension->SetRemote(false); // TODO(cosideci): this should be done differently. // Send an ownership claim request, and have the server broadcast the result. // Only then should components be added or removed. m_world.emplace_or_replace(acEntity, acServerId); m_world.emplace_or_replace(acEntity); DeleteRemoteEntityComponents(acEntity); RequestOwnershipClaim request; request.ServerId = acServerId; request.NewActorData = BuildActorData(pActor); m_transport.Send(request); return true; } void CharacterService::DeleteTempActor(const uint32_t aFormId) noexcept { Actor* pActor = Cast(TESForm::GetById(aFormId)); if (pActor && ((pActor->formID & 0xFF000000) == 0xFF000000)) { pActor->Delete(); spdlog::info("\tDeleted actor {:X}", aFormId); } } void CharacterService::OnActorAdded(const ActorAddedEvent& acEvent) noexcept { Actor* pActor = Cast(TESForm::GetById(acEvent.FormId)); if (acEvent.FormId == 0x14) { pActor->GetExtension()->SetPlayer(true); } entt::entity entity; const auto view = m_world.view(); const auto it = std::find_if( std::begin(view), std::end(view), [&acEvent, view](entt::entity entity) { auto& remoteComponent = view.get(entity); return remoteComponent.CachedRefId == acEvent.FormId; }); if (it != std::end(view)) { Actor* pActor = Cast(TESForm::GetById(acEvent.FormId)); pActor->GetExtension()->SetRemote(true); entity = *it; } else entity = m_world.create(); m_world.emplace_or_replace(entity, acEvent.FormId); m_world.emplace_or_replace(entity); ProcessNewEntity(entity); } void CharacterService::OnActorRemoved(const ActorRemovedEvent& acEvent) noexcept { auto view = m_world.view(); const auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.FormId](auto aEntity) { return view.get(aEntity).Id == formId; }); if (entityIt == view.end()) { spdlog::error("Actor to remove not found in form ids map {:X}", acEvent.FormId); return; } const auto cId = *entityIt; auto& formIdComponent = view.get(cId); CancelServerAssignment(*entityIt, formIdComponent.Id); m_world.remove(cId); if (m_world.all_of(cId)) m_world.remove(cId); if (m_world.orphan(cId)) m_world.destroy(cId); spdlog::info("Actor removed, form id: {:X}", acEvent.FormId); } void CharacterService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { RunSpawnUpdates(); RunLocalUpdates(); RunFactionsUpdates(); RunRemoteUpdates(); RunExperienceUpdates(); ApplyCachedWeaponDraws(acUpdateEvent); } void CharacterService::OnConnected(const ConnectedEvent& acConnectedEvent) const noexcept { // Go through all the forms that were previously detected auto view = m_world.view(entt::exclude); Vector entities(view.begin(), view.end()); for (auto entity : entities) { auto& formIdComponent = m_world.get(entity); // Delete all temporary actors on connect if (formIdComponent.Id > 0xFF000000) { Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (pActor) pActor->Delete(); continue; } ProcessNewEntity(entity); } } void CharacterService::OnDisconnected(const DisconnectedEvent& acDisconnectedEvent) const noexcept { auto remoteView = m_world.view(); for (auto entity : remoteView) { auto& formIdComponent = remoteView.get(entity); auto pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) continue; if (pActor->GetExtension()->IsRemotePlayer()) pActor->Delete(); else pActor->GetExtension()->SetRemote(false); } m_world.clear(); } void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessage) noexcept { spdlog::info("Received for cookie {:X}, server id {:X}", acMessage.Cookie, acMessage.ServerId); auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [view, cookie = acMessage.Cookie](auto entity) { return view.get(entity).Cookie == cookie; }); if (itor == std::end(view)) { spdlog::warn("Never found requested cookie: {}", acMessage.Cookie); return; } const auto cEntity = *itor; m_world.remove(cEntity); #if (!IS_MASTER) m_world.remove(cEntity); #endif const auto formIdComponent = m_world.try_get(cEntity); if (!formIdComponent) { spdlog::error(__FUNCTION__ ": form id component doesn't exist, cookie: {:X}", acMessage.Cookie); return; } Actor* pActor = Cast(TESForm::GetById(formIdComponent->Id)); if (!pActor) { spdlog::error(__FUNCTION__ ": actor not found, form id: {:X}", formIdComponent->Id); m_world.destroy(cEntity); return; } // TODO: how could this possibly trigger? // it's kinda interfering with my WaitingFor3D code if (acMessage.PlayerId != 0) m_world.emplace_or_replace(cEntity, acMessage.PlayerId); if (acMessage.Owner) { spdlog::info("Received local actor, form id: {:X}", pActor->formID); m_world.emplace_or_replace(cEntity, acMessage.ServerId); auto& localAnimationComponent = m_world.emplace_or_replace(cEntity); pActor->GetExtension()->SetRemote(false); if (auto* pEarlyAnimComponent = m_world.try_get(cEntity)) { for (const auto& action : pEarlyAnimComponent->Actions) { localAnimationComponent.Append(action); } } m_world.remove(cEntity); } else { spdlog::info("Received remote actor, form id: {:X}, isweapondrawn: {}", pActor->formID, acMessage.IsWeaponDrawn); m_world.emplace_or_replace(cEntity, acMessage.ServerId, formIdComponent->Id); pActor->GetExtension()->SetRemote(true); m_world.remove(cEntity); InterpolationSystem::Setup(m_world, cEntity); AnimationSystem::Setup(m_world, cEntity); AnimationSystem::AddActionsForReplay(m_world.get(cEntity), acMessage.ActionsToReplay); #if (!IS_MASTER) m_world.emplace_or_replace(cEntity, acMessage.ActionsToReplay); #endif pActor->SetActorValues(acMessage.AllActorValues); pActor->SetActorInventory(acMessage.CurrentInventory); if (pActor->IsDead() != acMessage.IsDead) acMessage.IsDead ? pActor->Kill() : pActor->Respawn(); m_weaponDrawUpdates[pActor->formID] = {acMessage.IsWeaponDrawn}; MoveActor(pActor, acMessage.WorldSpaceId, acMessage.CellId, acMessage.Position); } } void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage) const noexcept { auto remoteView = m_world.view(); const auto remoteItor = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); if (remoteItor != std::end(remoteView)) { spdlog::warn("Character with remote id {:X} is already spawned.", acMessage.ServerId); return; } Actor* pActor = nullptr; std::optional entity; // Custom forms if (acMessage.FormId == GameId{}) { TESNPC* pNpc = nullptr; entity = m_world.create(); if (acMessage.BaseId != GameId{}) { const auto cNpcId = World::Get().GetModSystem().GetGameId(acMessage.BaseId); if (cNpcId == 0) { spdlog::error("Failed to retrieve NPC, it will not be spawned, possibly missing mod, base: {:X}:{:X}, form: {:X}:{:X}", acMessage.BaseId.BaseId, acMessage.BaseId.ModId, acMessage.FormId.BaseId, acMessage.FormId.ModId); return; } pNpc = Cast(TESForm::GetById(cNpcId)); pNpc->Deserialize(acMessage.AppearanceBuffer, acMessage.ChangeFlags); } else { // Players and npcs with temporary ref ids and base ids (usually random events) pNpc = TESNPC::Create(acMessage.AppearanceBuffer, acMessage.ChangeFlags); FaceGenSystem::Setup(m_world, *entity, acMessage.FaceTints); } pActor = Actor::Create(pNpc); } else { const uint32_t cActorId = World::Get().GetModSystem().GetGameId(acMessage.FormId); auto waitingView = m_world.view(); const auto waitingItor = std::find_if(std::begin(waitingView), std::end(waitingView), [waitingView, cActorId](auto entity) { return waitingView.get(entity).Id == cActorId; }); if (waitingItor != std::end(waitingView)) { spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId); return; } auto* const pForm = TESForm::GetById(cActorId); pActor = Cast(pForm); if (!pActor) { spdlog::error("Failed to retrieve Actor {:X}, it will not be spawned, possibly missing mod", cActorId); spdlog::error("\tForm : {:X}", pForm ? pForm->formID : 0); return; } const auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [cActorId, view](entt::entity entity) { return view.get(entity).Id == cActorId; }); if (itor != std::end(view)) entity = *itor; else entity = m_world.create(); } if (!pActor) { spdlog::error("Actor object {:X} could not be created.", acMessage.ServerId); return; } spdlog::info("CharacterSpawnRequest, server id: {:X}, form id: {:X}", acMessage.ServerId, pActor->formID); if (pActor->IsDisabled()) { spdlog::warn("Disabled actor is being re-enabled: {:X}", pActor->formID); pActor->EnableImpl(); } pActor->GetExtension()->SetRemote(true); pActor->rotation.x = acMessage.Rotation.x; pActor->rotation.z = acMessage.Rotation.y; pActor->MoveTo(PlayerCharacter::Get()->parentCell, acMessage.Position); pActor->SetActorValues(acMessage.InitialActorValues); pActor->GetExtension()->SetPlayer(acMessage.IsPlayer); if (acMessage.IsPlayer) { pActor->SetIgnoreFriendlyHit(true); pActor->SetPlayerRespawnMode(); m_world.emplace_or_replace(*entity, acMessage.PlayerId); } if (pActor->IsDead() != acMessage.IsDead) acMessage.IsDead ? pActor->Kill() : pActor->Respawn(); spdlog::info("Spawn Request Is summon {}", acMessage.IsPlayerSummon); if (acMessage.IsPlayerSummon) { // Prevents remote summons agroing other players. pActor->SetCommandingActor(PlayerCharacter::Get()->GetHandle()); } auto& remoteComponent = m_world.emplace_or_replace(*entity, acMessage.ServerId, pActor->formID); auto& interpolationComponent = InterpolationSystem::Setup(m_world, *entity); interpolationComponent.Position = acMessage.Position; AnimationSystem::Setup(m_world, *entity); m_world.emplace_or_replace(*entity, acMessage); auto& remoteAnimationComponent = m_world.get(*entity); AnimationSystem::AddActionsForReplay(remoteAnimationComponent, acMessage.ActionsToReplay); #if (!IS_MASTER) m_world.emplace_or_replace(*entity, acMessage.ActionsToReplay); #endif } void CharacterService::OnRemoteSpawnDataReceived(const NotifySpawnData& acMessage) noexcept { auto view = m_world.view(entt::exclude); const auto itor = std::find_if( std::begin(view), std::end(view), [view, id = acMessage.Id](auto entity) { if (auto serverId = Utils::GetServerId(entity)) { if (*serverId == id) return true; } return false; }); if (itor == std::end(view)) return; if (auto* pWaitingFor3D = m_world.try_get(*itor)) { pWaitingFor3D->SpawnRequest.InitialActorValues = acMessage.NewActorData.InitialActorValues; pWaitingFor3D->SpawnRequest.InventoryContent = acMessage.NewActorData.InitialInventory; pWaitingFor3D->SpawnRequest.IsDead = acMessage.NewActorData.IsDead; pWaitingFor3D->SpawnRequest.IsWeaponDrawn = acMessage.NewActorData.IsWeaponDrawn; } auto& formIdComponent = view.get(*itor); Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) return; pActor->SetActorValues(acMessage.NewActorData.InitialActorValues); pActor->SetActorInventory(acMessage.NewActorData.InitialInventory); m_weaponDrawUpdates[pActor->formID] = {acMessage.NewActorData.IsWeaponDrawn}; if (pActor->IsDead() != acMessage.NewActorData.IsDead) acMessage.NewActorData.IsDead ? pActor->Kill() : pActor->Respawn(); spdlog::info("Applied remote spawn data, actor form id: {:X}", pActor->formID); } void CharacterService::OnReferencesMoveRequest(const ServerReferencesMoveRequest& acMessage) const noexcept { auto view = m_world.view(); for (const auto& [serverId, update] : acMessage.Updates) { auto itor = std::find_if(std::begin(view), std::end(view), [serverId = serverId, view](entt::entity entity) { return view.get(entity).Id == serverId; }); if (itor == std::end(view)) continue; auto& interpolationComponent = view.get(*itor); auto& animationComponent = view.get(*itor); const auto& movement = update.UpdatedMovement; InterpolationComponent::TimePoint point; point.Tick = acMessage.Tick; point.Position = movement.Position; point.Rotation = {movement.Rotation.x, 0.f, movement.Rotation.y}; point.Variables = movement.Variables; point.Direction = movement.Direction; InterpolationSystem::AddPoint(interpolationComponent, point); for (const auto& action : update.ActionEvents) { animationComponent.TimePoints.push_back(action); } } } void CharacterService::OnActionEvent(const ActionEvent& acActionEvent) const noexcept { auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [id = acActionEvent.ActorId, view](entt::entity entity) { return view.get(entity).Id == id; }); if (itor != std::end(view)) { auto& localComponent = view.get(*itor); localComponent.Append(acActionEvent); } else if (m_transport.IsOnline()) { // A `LocalAnimationComponent` is not attached yet, but the actor already exists and is running animations auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [id = acActionEvent.ActorId, view](entt::entity entity) { return view.get(entity).Id == id; }); if (itor != std::end(view)) { view.get(*itor).Actions.push_back(acActionEvent); } } } void CharacterService::OnFactionsChanges(const NotifyFactionsChanges& acEvent) const noexcept { auto view = m_world.view(); for (const auto& [id, factions] : acEvent.Changes) { const auto itor = std::find_if(std::begin(view), std::end(view), [id = id, view](entt::entity entity) { return view.get(entity).Id == id; }); if (itor != std::end(view)) { auto& formIdComponent = view.get(*itor); auto* const pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) return; auto& cacheComponent = view.get(*itor); cacheComponent.FactionsContent = factions; pActor->SetFactions(cacheComponent.FactionsContent); } } } void CharacterService::OnOwnershipTransfer(const NotifyOwnershipTransfer& acMessage) const noexcept { // TODO(cosideci): handle case if no one has it, therefore no RemoteComponent auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [&acMessage, &view](auto entity) { return view.get(entity).Id == acMessage.ServerId; }); if (itor != std::end(view)) { auto& formIdComponent = view.get(*itor); if (TakeOwnership(formIdComponent.Id, acMessage.ServerId, *itor)) { spdlog::info("Ownership claimed {:X}", acMessage.ServerId); return; } } spdlog::warn("Actor for ownership transfer not found {:X}", acMessage.ServerId); RequestOwnershipTransfer request{}; request.ServerId = acMessage.ServerId; m_transport.Send(request); } void CharacterService::OnRemoveCharacter(const NotifyRemoveCharacter& acMessage) const noexcept { auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [id = acMessage.ServerId, view](entt::entity entity) { return view.get(entity).Id == id; }); if (itor != std::end(view)) { if (auto* pFormIdComponent = m_world.try_get(*itor)) CharacterService::DeleteTempActor(pFormIdComponent->Id); DeleteRemoteEntityComponents(*itor); } } void CharacterService::OnNotifyRespawn(const NotifyRespawn& acMessage) const noexcept { 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("Actor to respawn not found in: {:X}", acMessage.ActorId); return; } const auto cId = *entityIt; auto& formIdComponent = view.get(cId); CancelServerAssignment(*entityIt, formIdComponent.Id); m_world.remove(cId); if (m_world.all_of(cId)) m_world.remove(cId); if (m_world.orphan(cId)) m_world.destroy(cId); RequestRespawn request; request.ActorId = acMessage.ActorId; m_transport.Send(request); } void CharacterService::OnBeastFormChange(const BeastFormChangeEvent& acEvent) const noexcept { auto view = m_world.view(); const auto it = std::find_if(view.begin(), view.end(), [view](auto entity) { return view.get(entity).Id == 0x14; }); std::optional serverIdRes = Utils::GetServerId(*it); if (!serverIdRes.has_value()) { spdlog::error("{}: failed to find server id", __FUNCTION__); return; } uint32_t serverId = serverIdRes.value(); RequestRespawn request; request.ActorId = serverId; Actor* pActor = Utils::GetByServerId(serverId); if (!pActor) { spdlog::warn(__FUNCTION__ ": could not find actor for server id {:X}", serverId); return; } TESNPC* pNpc = Cast(pActor->baseForm); if (!pNpc) { spdlog::warn(__FUNCTION__ ": could not find actor baseform for server id {:X}", serverId); return; } pNpc->Serialize(&request.AppearanceBuffer); request.ChangeFlags = pNpc->GetChangeFlags(); m_transport.Send(request); } void CharacterService::OnMountEvent(const MountEvent& acEvent) const noexcept { auto view = m_world.view(); const auto riderIt = std::find_if(std::begin(view), std::end(view), [id = acEvent.RiderID, view](auto entity) { return view.get(entity).Id == id; }); if (riderIt == std::end(view)) { spdlog::warn("Rider not found, form id: {:X}", acEvent.RiderID); return; } const entt::entity cRiderEntity = *riderIt; std::optional riderServerIdRes = Utils::GetServerId(cRiderEntity); if (!riderServerIdRes.has_value()) { spdlog::error("{}: failed to find server id", __FUNCTION__); return; } const auto mountIt = std::find_if(std::begin(view), std::end(view), [id = acEvent.MountID, view](auto entity) { return view.get(entity).Id == id; }); if (mountIt == std::end(view)) { spdlog::warn("Mount not found, form id: {:X}", acEvent.MountID); return; } const entt::entity cMountEntity = *mountIt; std::optional mountServerIdRes = Utils::GetServerId(cMountEntity); if (!mountServerIdRes.has_value()) { spdlog::error("{}: failed to find server id", __FUNCTION__); return; } if (m_world.try_get(cMountEntity)) TakeOwnership(acEvent.MountID, *mountServerIdRes, cMountEntity); MountRequest request; request.MountId = mountServerIdRes.value(); request.RiderId = riderServerIdRes.value(); m_transport.Send(request); } void CharacterService::OnNotifyMount(const NotifyMount& acMessage) const noexcept { auto remoteView = m_world.view(); const auto riderIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.RiderId](auto entity) { return remoteView.get(entity).Id == Id; }); if (riderIt == std::end(remoteView)) { spdlog::warn("Rider with remote id {:X} not found.", acMessage.RiderId); return; } auto& riderFormIdComponent = remoteView.get(*riderIt); TESForm* pRiderForm = TESForm::GetById(riderFormIdComponent.Id); Actor* pRider = Cast(pRiderForm); Actor* pMount = nullptr; auto formView = m_world.view(); Vector entities(formView.begin(), formView.end()); // TODO(cosideci): remove this, cause of NotifyRelinquishControl? for (auto entity : entities) { std::optional serverIdRes = Utils::GetServerId(entity); if (!serverIdRes.has_value()) { spdlog::error("{}: failed to find server id", __FUNCTION__); continue; } uint32_t serverId = serverIdRes.value(); if (serverId == acMessage.MountId) { auto& mountFormIdComponent = m_world.get(entity); if (m_world.all_of(entity)) { m_world.remove(entity); m_world.emplace_or_replace(entity, acMessage.MountId, mountFormIdComponent.Id); } TESForm* pMountForm = TESForm::GetById(mountFormIdComponent.Id); pMount = Cast(pMountForm); pMount->GetExtension()->SetRemote(true); InterpolationSystem::Setup(m_world, entity); AnimationSystem::Setup(m_world, entity); break; } } pRider->InitiateMountPackage(pMount); } void CharacterService::OnInitPackageEvent(const InitPackageEvent& acEvent) const noexcept { if (!m_transport.IsConnected()) return; auto view = m_world.view(); const auto actorIt = std::find_if(std::begin(view), std::end(view), [id = acEvent.ActorId, view](auto entity) { return view.get(entity).Id == id; }); if (actorIt == std::end(view)) return; const entt::entity cActorEntity = *actorIt; std::optional actorServerIdRes = Utils::GetServerId(cActorEntity); if (!actorServerIdRes.has_value()) { spdlog::error("{}: failed to find server id", __FUNCTION__); return; } NewPackageRequest request; request.ActorId = actorServerIdRes.value(); if (!m_world.GetModSystem().GetServerModId(acEvent.PackageId, request.PackageId.ModId, request.PackageId.BaseId)) return; m_transport.Send(request); } void CharacterService::OnNotifyNewPackage(const NotifyNewPackage& acMessage) const noexcept { auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ActorId](auto entity) { return remoteView.get(entity).Id == Id; }); if (remoteIt == std::end(remoteView)) { spdlog::warn("Actor for package with remote id {:X} not found.", acMessage.ActorId); return; } auto formIdComponent = remoteView.get(*remoteIt); const TESForm* pForm = TESForm::GetById(formIdComponent.Id); Actor* pActor = Cast(pForm); const uint32_t cPackageFormId = World::Get().GetModSystem().GetGameId(acMessage.PackageId); const TESForm* pPackageForm = TESForm::GetById(cPackageFormId); if (!pPackageForm) { spdlog::warn("Actor package not found, base id: {:X}, mod id: {:X}", acMessage.PackageId.BaseId, acMessage.PackageId.ModId); return; } TESPackage* pPackage = Cast(pPackageForm); pActor->SetPackage(pPackage); } void CharacterService::OnAddExperienceEvent(const AddExperienceEvent& acEvent) noexcept { m_cachedExperience += acEvent.Experience; } void CharacterService::OnNotifySyncExperience(const NotifySyncExperience& acMessage) noexcept { PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (PlayerCharacter::LastUsedCombatSkill == -1) return; pPlayer->AddSkillExperience(PlayerCharacter::LastUsedCombatSkill, acMessage.Experience); } void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; auto view = m_world.view(entt::exclude); auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.ActorID](auto entity) { return view.get(entity).Id == formId; }); if (entityIt == view.end()) return; auto serverIdRes = Utils::GetServerId(*entityIt); if (!serverIdRes) { spdlog::error("{}: server id not found for form id {:X}", __FUNCTION__, acEvent.ActorID); return; } DialogueRequest request{}; request.ServerId = serverIdRes.value(); request.SoundFilename = acEvent.VoiceFile; m_transport.Send(request); } void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcept { auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); if (remoteIt == std::end(remoteView)) { spdlog::warn("Actor for dialogue with remote id {:X} not found.", acMessage.ServerId); return; } auto formIdComponent = remoteView.get(*remoteIt); const TESForm* pForm = TESForm::GetById(formIdComponent.Id); Actor* pActor = Cast(pForm); if (!pActor) return; pActor->StopCurrentDialogue(true); pActor->SpeakSound(acMessage.SoundFilename.c_str()); } void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; auto view = m_world.view(entt::exclude); auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.SpeakerID](auto entity) { return view.get(entity).Id == formId; }); if (entityIt == view.end()) return; auto serverIdRes = Utils::GetServerId(*entityIt); if (!serverIdRes) { spdlog::error("{}: server id not found for form id {:X}", __FUNCTION__, acEvent.SpeakerID); return; } SubtitleRequest request{}; request.ServerId = serverIdRes.value(); request.Text = acEvent.Text; request.TopicFormId = acEvent.TopicFormID; m_transport.Send(request); } void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcept { auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); if (remoteIt == std::end(remoteView)) { spdlog::warn("Actor for dialogue with remote id {:X} not found.", acMessage.ServerId); return; } auto formIdComponent = remoteView.get(*remoteIt); const TESForm* pForm = TESForm::GetById(formIdComponent.Id); Actor* pActor = Cast(pForm); if (!pActor) return; // This is only for fallout 4 TESTopicInfo* pInfo = nullptr; pInfo = Cast(TESForm::GetById(acMessage.TopicFormId)); SubtitleManager::Get()->ShowSubtitle(pActor, acMessage.Text.c_str(), pInfo); } void CharacterService::OnNotifyRelinquishControl(const NotifyRelinquishControl& acMessage) noexcept { auto formView = m_world.view(); Vector entities(formView.begin(), formView.end()); // TODO(cosideci): this entity iteration shouldn't technically be necessary, just look for the local component for (auto entity : entities) { std::optional serverIdRes = Utils::GetServerId(entity); if (!serverIdRes.has_value()) { spdlog::error(__FUNCTION__ ": failed to find server id for entity"); continue; } uint32_t serverId = serverIdRes.value(); if (serverId == acMessage.ServerId) { auto& formIdComponent = m_world.get(entity); if (m_world.all_of(entity)) { m_world.remove(entity); m_world.emplace_or_replace(entity, acMessage.ServerId, formIdComponent.Id); } Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) { // Probably left the room and/or temporary. spdlog::info(__FUNCTION__ ": no local Actor for serverId {:X} to relinquish", serverId); continue; } pActor->GetExtension()->SetRemote(true); InterpolationSystem::Setup(m_world, entity); AnimationSystem::Setup(m_world, entity); spdlog::info(__FUNCTION__ ": relinquished control of actor {:X} with server id {:X}", pActor->formID, acMessage.ServerId); return; } } spdlog::error("Did not find actor to relinquish control of, server id {:X}", acMessage.ServerId); } void CharacterService::OnNotifyActorTeleport(const NotifyActorTeleport& acMessage) noexcept { auto& modSystem = m_world.GetModSystem(); const uint32_t cActorId = World::Get().GetModSystem().GetGameId(acMessage.FormId); Actor* pActor = Cast(TESForm::GetById(cActorId)); if (!pActor) { spdlog::error(__FUNCTION__ ": failed to retrieve actor to teleport."); return; } MoveActor(pActor, acMessage.WorldSpaceId, acMessage.CellId, acMessage.Position); spdlog::info("Successfully teleported actor, form id: {:X}, world space: {:X}, cell: {:X}, position: ({}, {}, {})", pActor->formID, acMessage.WorldSpaceId.BaseId, acMessage.CellId.BaseId, acMessage.Position.x, acMessage.Position.y, acMessage.Position.z); } void CharacterService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { // Takes ownership of all actors if (acEvent.IsLeader) { auto view = m_world.view(entt::exclude); Vector entities(view.begin(), view.end()); for (auto entity : entities) ProcessNewEntity(entity); } } void CharacterService::MoveActor(const Actor* apActor, const GameId& acWorldSpaceId, const GameId& acCellId, const Vector3_NetQuantize& acPosition) const noexcept { TESObjectCELL* pCell = nullptr; if (!acWorldSpaceId) { const uint32_t cCellId = m_world.GetModSystem().GetGameId(acCellId); pCell = Cast(TESForm::GetById(cCellId)); } // In case of lazy-loading of exterior cells else { const uint32_t cWorldSpaceId = m_world.GetModSystem().GetGameId(acWorldSpaceId); TESWorldSpace* const pWorldSpace = Cast(TESForm::GetById(cWorldSpaceId)); if (pWorldSpace) { GridCellCoords coordinates = GridCellCoords::CalculateGridCellCoords(acPosition); pCell = pWorldSpace->LoadCell(coordinates.X, coordinates.Y); } } if (!pCell) { spdlog::error(__FUNCTION__ ": failed to fetch cell to teleport, actor: {:X}, worldspace: {:X}, cell: {:X}, position: {}, {}, {}", apActor->formID, acWorldSpaceId.BaseId, acCellId.BaseId, acPosition.x, acPosition.y, acPosition.z); return; } apActor->MoveTo(pCell, acPosition); } void CharacterService::ProcessNewEntity(entt::entity aEntity) const noexcept { if (!m_transport.IsOnline()) return; auto& formIdComponent = m_world.get(aEntity); Actor* const pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) { spdlog::warn(__FUNCTION__ ": actor for new entity not found, form id: {:X}", formIdComponent.Id); return; } if (auto* pRemoteComponent = m_world.try_get(aEntity); pRemoteComponent) { // TODO(cosideci): don't just take all actors (i.e. from other parties), // maybe check it server side, add a variable to the request. if (m_world.GetPartyService().IsLeader() && !pActor->IsTemporary() && !pActor->IsMount()) { spdlog::info("Sending ownership claim for actor {:X} with server id {:X}", pActor->formID, pRemoteComponent->Id); TakeOwnership(pActor->formID, pRemoteComponent->Id, aEntity); } else spdlog::info("New entity remotely managed, form id: {:X}, server id: {:X}", pActor->formID, pRemoteComponent->Id); return; } if (m_world.any_of(aEntity)) return; CacheSystem::Setup(World::Get(), aEntity, pActor); RequestServerAssignment(aEntity); } void CharacterService::RequestServerAssignment(const entt::entity aEntity) const noexcept { if (!m_transport.IsOnline()) return; static uint32_t sCookieSeed = 0; const auto& formIdComponent = m_world.get(aEntity); auto* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) return; TESNPC* pNpc = Cast(pActor->baseForm); if (!pNpc) return; AssignCharacterRequest message{}; message.Cookie = sCookieSeed; if (!m_world.GetModSystem().GetServerModId(formIdComponent.Id, message.ReferenceId)) { spdlog::error("Server reference id not found for form id {:X}", formIdComponent.Id); return; } if (!m_world.GetModSystem().GetServerModId(pActor->parentCell->formID, message.CellId)) { spdlog::error("Server cell id not found for cell id {:X}", pActor->parentCell->formID); return; } if (const auto pWorldSpace = pActor->GetWorldSpace()) { if (!m_world.GetModSystem().GetServerModId(pWorldSpace->formID, message.WorldSpaceId)) return; } message.Position = pActor->position; message.Rotation.x = pActor->rotation.x; message.Rotation.y = pActor->rotation.z; // Serialize the base form const auto isPlayer = (formIdComponent.Id == 0x14); const auto isTemporary = pActor->formID >= 0xFF000000; if (isPlayer) { pNpc->MarkChanged(0x2000800); } const auto changeFlags = pNpc->GetChangeFlags(); if (isPlayer || changeFlags != 0) { message.ChangeFlags = changeFlags; pNpc->Serialize(&message.AppearanceBuffer); } if (isPlayer) { auto& entries = message.FaceTints.Entries; const auto& tints = PlayerCharacter::Get()->GetTints(); entries.resize(tints.length); for (auto i = 0u; i < tints.length; ++i) { entries[i].Alpha = tints[i]->alpha; entries[i].Color = tints[i]->color; entries[i].Type = tints[i]->type; if (tints[i]->texture) entries[i].Name = tints[i]->texture->name.AsAscii(); } } if (isPlayer) { auto& questLog = message.QuestContent.Entries; auto& modSystem = m_world.GetModSystem(); for (const auto& objective : PlayerCharacter::Get()->objectives) { auto* pQuest = objective.instance->quest; if (!pQuest) continue; if (!QuestService::IsNonSyncableQuest(pQuest)) { GameId id{}; if (modSystem.GetServerModId(pQuest->formID, id)) { auto& entry = questLog.emplace_back(); entry.Stage = pQuest->currentStage; entry.Id = id; } } } // remove duplicates const auto ip = std::unique(questLog.begin(), questLog.end()); questLog.resize(std::distance(questLog.begin(), ip)); } message.CurrentActorData = BuildActorData(pActor); message.FactionsContent = pActor->GetFactions(); message.IsDragon = pActor->IsDragon(); message.IsMount = pActor->IsMount(); message.IsPlayerSummon = pActor->GetCommandingActor() && pActor->GetCommandingActor()->formID == 0x14; if (pNpc->IsTemporary()) pNpc = pNpc->GetTemplateBase(); if (isTemporary) { if (pNpc && !m_world.GetModSystem().GetServerModId(pNpc->formID, message.FormId)) { spdlog::error("Server NPC form id not found for form id {:X}", pNpc->formID); return; } } // Serialize actions auto* const pExtension = pActor->GetExtension(); message.LatestAction = pExtension->LatestAnimation; pActor->SaveAnimationVariables(message.LatestAction.Variables); spdlog::info("Request id: {:X}, cookie: {:X}, entity: {:X}", formIdComponent.Id, sCookieSeed, to_integral(aEntity)); if (m_transport.Send(message)) { m_world.emplace(aEntity, sCookieSeed); sCookieSeed++; } } void CharacterService::CancelServerAssignment(const entt::entity aEntity, const uint32_t aFormId) const noexcept { if (m_world.all_of(aEntity)) { Actor* pActor = Cast(TESForm::GetById(aFormId)); if (pActor) { if (pActor->IsTemporary()) { spdlog::info("Temporary Remote Deleted {:X}", aFormId); pActor->Delete(); } else { pActor->GetExtension()->SetRemote(false); } } DeleteRemoteEntityComponents(aEntity); return; } // In the event we were waiting for assignment, drop it if (m_world.all_of(aEntity)) { auto& waitingComponent = m_world.get(aEntity); CancelAssignmentRequest message; message.Cookie = waitingComponent.Cookie; m_transport.Send(message); m_world.remove(aEntity); } if (m_world.all_of(aEntity)) { auto& localComponent = m_world.get(aEntity); RequestOwnershipTransfer request{}; request.ServerId = localComponent.Id; if (Actor* pActor = Cast(TESForm::GetById(aFormId))) { if (!pActor->IsTemporary()) { auto& modSystem = m_world.GetModSystem(); if (TESWorldSpace* pWorldSpace = pActor->GetWorldSpace()) { if (!modSystem.GetServerModId(pWorldSpace->formID, request.WorldSpaceId)) spdlog::error("World space id not found, despite having a world space, {:X}", pWorldSpace->formID); } if (TESObjectCELL* pCell = pActor->GetParentCell()) { if (!modSystem.GetServerModId(pCell->formID, request.CellId)) spdlog::error("Cell id not found, despite having a cell, {:X}", pCell->formID); } request.Position = pActor->position; } } spdlog::info( "Transferring ownership of local actor, server id: {:X}, worldspace: {:X}, cell: {:X}, position: " "({}, {}, {})", request.ServerId, request.WorldSpaceId.BaseId, request.CellId.BaseId, request.Position.x, request.Position.y, request.Position.z); m_transport.Send(request); m_world.remove(aEntity); } } Actor* CharacterService::CreateCharacterForEntity(entt::entity aEntity) const noexcept { auto* pWaitingFor3D = m_world.try_get(aEntity); auto* pInterpolationComponent = m_world.try_get(aEntity); if (!pWaitingFor3D || !pInterpolationComponent) { spdlog::error(__FUNCTION__ ": could not find WaitingFor3D or InterpolationComponent"); return nullptr; } auto& acMessage = pWaitingFor3D->SpawnRequest; Actor* pActor = nullptr; // Custom forms if (acMessage.FormId == GameId{}) { TESNPC* pNpc = nullptr; if (acMessage.BaseId != GameId{}) { const uint32_t cNpcId = World::Get().GetModSystem().GetGameId(acMessage.BaseId); if (cNpcId == 0) { spdlog::error("Failed to retrieve NPC, it will not be spawned, possibly missing mod"); return nullptr; } pNpc = Cast(TESForm::GetById(cNpcId)); pNpc->Deserialize(acMessage.AppearanceBuffer, acMessage.ChangeFlags); } else { pNpc = TESNPC::Create(acMessage.AppearanceBuffer, acMessage.ChangeFlags); FaceGenSystem::Setup(m_world, aEntity, acMessage.FaceTints); } pActor = Actor::Create(pNpc); } auto& remoteComponent = m_world.get(aEntity); if (!pActor) { spdlog::error(__FUNCTION__ ": could not spawn actor for remote server id {:X}.", remoteComponent.Id); return nullptr; } pActor->GetExtension()->SetRemote(true); pActor->rotation.x = acMessage.Rotation.x; pActor->rotation.z = acMessage.Rotation.y; pActor->MoveTo(PlayerCharacter::Get()->parentCell, pInterpolationComponent->Position); pActor->SetActorValues(acMessage.InitialActorValues); pActor->GetExtension()->SetPlayer(acMessage.IsPlayer); if (acMessage.IsPlayer) { pActor->SetIgnoreFriendlyHit(true); pActor->SetPlayerRespawnMode(); m_world.emplace_or_replace(aEntity, acMessage.PlayerId); } if (pActor->IsDead() != acMessage.IsDead) acMessage.IsDead ? pActor->Kill() : pActor->Respawn(); spdlog::info("Spawned character for entity, server id: {:X}", remoteComponent.Id); return pActor; } ActorData CharacterService::BuildActorData(Actor* apActor) const noexcept { ActorData actorData{}; actorData.InitialActorValues = apActor->GetEssentialActorValues(); actorData.InitialInventory = apActor->GetActorInventory(); actorData.IsDead = apActor->IsDead(); actorData.IsWeaponDrawn = apActor->actorState.IsWeaponFullyDrawn(); return actorData; } void CharacterService::RunLocalUpdates() const noexcept { static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenSnapshots = 100ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenSnapshots) return; lastSendTimePoint = now; ClientReferencesMoveRequest message; message.Tick = m_transport.GetClock().GetCurrentTick(); auto animatedLocalView = m_world.view(); for (auto entity : animatedLocalView) { auto& localComponent = animatedLocalView.get(entity); auto& animationComponent = animatedLocalView.get(entity); auto& formIdComponent = animatedLocalView.get(entity); AnimationSystem::Serialize(m_world, message, localComponent, animationComponent, formIdComponent); } m_transport.Send(message); } void CharacterService::RunRemoteUpdates() noexcept { // Delay by 300ms to let the interpolation system accumulate interpolation points const auto tick = m_transport.GetClock().GetCurrentTick() - 300; // Interpolation has to keep running even if the actor is not in view, otherwise we will never know if we need to spawn it auto interpolatedEntities = m_world.view(); for (auto entity : interpolatedEntities) { auto* pFormIdComponent = m_world.try_get(entity); auto& interpolationComponent = interpolatedEntities.get(entity); Actor* pActor = nullptr; if (pFormIdComponent) { auto* pForm = TESForm::GetById(pFormIdComponent->Id); pActor = Cast(pForm); } InterpolationSystem::Update(pActor, interpolationComponent, tick); } auto animatedView = m_world.view(); for (auto entity : animatedView) { auto& animationComponent = animatedView.get(entity); auto& formIdComponent = animatedView.get(entity); auto* pForm = TESForm::GetById(formIdComponent.Id); auto* pActor = Cast(pForm); if (!pActor) continue; AnimationSystem::Update(m_world, pActor, animationComponent, tick); } auto facegenView = m_world.view(); for (auto entity : facegenView) { auto& formIdComponent = facegenView.get(entity); auto& faceGenComponent = facegenView.get(entity); const auto* pForm = TESForm::GetById(formIdComponent.Id); auto* pActor = Cast(pForm); if (!pActor) continue; FaceGenSystem::Update(m_world, pActor, faceGenComponent); } auto waitingView = m_world.view(); Vector toRemove; for (auto entity : waitingView) { auto& formIdComponent = waitingView.get(entity); auto& waitingFor3D = waitingView.get(entity); Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor || !pActor->GetNiNode()) continue; // By now, the actor has materialized in the world and is ready for further setup pActor->SetActorInventory(waitingFor3D.SpawnRequest.InventoryContent); pActor->SetFactions(waitingFor3D.SpawnRequest.FactionsContent); if (!waitingFor3D.SpawnRequest.ActionsToReplay.Actions.empty()) { pActor->LoadAnimationVariables(waitingFor3D.SpawnRequest.ActionsToReplay.Actions[0].Variables); } m_weaponDrawUpdates[pActor->formID] = {waitingFor3D.SpawnRequest.IsWeaponDrawn}; if (pActor->IsDead() != waitingFor3D.SpawnRequest.IsDead) waitingFor3D.SpawnRequest.IsDead ? pActor->Kill() : pActor->Respawn(); if (pActor->IsVampireLord()) pActor->FixVampireLordModel(); toRemove.push_back(entity); spdlog::info("Applied 3D for actor, form id: {:X}", pActor->formID); } for (auto entity : toRemove) m_world.remove(entity); } void CharacterService::RunFactionsUpdates() const noexcept { static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenSnapshots = 2000ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenSnapshots) return; lastSendTimePoint = now; RequestFactionsChanges message; auto factionedActors = m_world.view(); for (auto entity : factionedActors) { auto& formIdComponent = factionedActors.get(entity); auto& localComponent = factionedActors.get(entity); auto& cacheComponent = factionedActors.get(entity); const auto* pForm = TESForm::GetById(formIdComponent.Id); const auto* pActor = Cast(pForm); if (!pActor) continue; // Check if cached factions and current factions are identical auto factions = pActor->GetFactions(); if (cacheComponent.FactionsContent == factions) continue; cacheComponent.FactionsContent = factions; // If not send the current factions and replace the cached factions message.Changes[localComponent.Id] = factions; } if (!message.Changes.empty()) m_transport.Send(message); } void CharacterService::RunSpawnUpdates() const noexcept { auto invisibleView = m_world.view(entt::exclude); Vector entities(invisibleView.begin(), invisibleView.end()); for (const auto entity : entities) { auto& remoteComponent = m_world.get(entity); auto& interpolationComponent = m_world.get(entity); if (const auto pWorldSpace = PlayerCharacter::Get()->GetWorldSpace()) { float characterX = interpolationComponent.Position.x; float characterY = interpolationComponent.Position.y; const auto characterCoords = GridCellCoords::CalculateGridCellCoords(characterX, characterY); const TES* pTES = TES::Get(); const auto playerCoords = GridCellCoords(pTES->centerGridX, pTES->centerGridY); // TODO(cosideci): IsDragon probably shouldn't be straight up false here. if (GridCellCoords::IsCellInGridCell(characterCoords, playerCoords, false)) { auto* pActor = Cast(TESForm::GetById(remoteComponent.CachedRefId)); if (!pActor) { pActor = CreateCharacterForEntity(entity); if (!pActor) continue; remoteComponent.CachedRefId = pActor->formID; } pActor->MoveTo(PlayerCharacter::Get()->parentCell, interpolationComponent.Position); } } } } void CharacterService::RunExperienceUpdates() noexcept { static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenSnapshots = 1000ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenSnapshots) return; lastSendTimePoint = now; if (m_cachedExperience == 0.f) return; if (!World::Get().GetPartyService().IsInParty()) return; SyncExperienceRequest message; message.Experience = m_cachedExperience; m_cachedExperience = 0.f; m_transport.Send(message); spdlog::debug("Sending over experience {}", message.Experience); } void CharacterService::ApplyCachedWeaponDraws(const UpdateEvent& acUpdateEvent) noexcept { std::vector toRemove{}; for (auto& [cId, _] : m_weaponDrawUpdates) { auto& data = m_weaponDrawUpdates[cId]; data.m_timer += acUpdateEvent.Delta; // We do 2 passes because Skyrim's weapon drawing is the most finnicky thing in existence double maxTime = data.m_isFirstPass ? 0.5 : 2.0; if (data.m_timer <= maxTime) continue; Actor* pActor = Cast(TESForm::GetById(cId)); if (!pActor) continue; pActor->SetWeaponDrawnEx(data.m_drawWeapon); if (!data.m_isFirstPass) toRemove.push_back(cId); data.m_isFirstPass = false; } for (uint32_t id : toRemove) m_weaponDrawUpdates.erase(id); }