#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ObjectService::ObjectService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) : m_world(aWorld) , m_transport(aTransport) { m_disconnectedConnection = aDispatcher.sink().connect<&ObjectService::OnDisconnected>(this); m_cellChangeConnection = aDispatcher.sink().connect<&ObjectService::OnCellChange>(this); m_onActivateConnection = aDispatcher.sink().connect<&ObjectService::OnActivate>(this); m_activateConnection = aDispatcher.sink().connect<&ObjectService::OnActivateNotify>(this); m_lockChangeConnection = aDispatcher.sink().connect<&ObjectService::OnLockChange>(this); m_lockChangeNotifyConnection = aDispatcher.sink().connect<&ObjectService::OnLockChangeNotify>(this); m_assignObjectConnection = aDispatcher.sink().connect<&ObjectService::OnAssignObjectsResponse>(this); m_scriptAnimationConnection = aDispatcher.sink().connect<&ObjectService::OnScriptAnimationEvent>(this); m_scriptAnimationNotifyConnection = aDispatcher.sink().connect<&ObjectService::OnNotifyScriptAnimation>(this); EventDispatcherManager::Get()->activateEvent.RegisterSink(this); } bool IsPlayerHome(const TESObjectCELL* pCell) noexcept { if (pCell && pCell->loadedCellData && pCell->loadedCellData->encounterZone) { // Only return true if cell has the NoResetZone encounter zone if (pCell->loadedCellData->encounterZone->formID == 0xf90b1) { switch (pCell->formID) { case 0xeec55: // one known exception: Sinderion's Field Lab return false; default: return true; } } } return false; } bool ShouldSyncObject(const TESObjectREFR* apObject) noexcept { if (!apObject) return false; switch (apObject->formID) { case 0x39CF1: // Don't sync the chest in the "Diplomatic Immunity" quest return false; case 0x3EF03: // ...as well as in the "No One Escapes Cidhna Mine" quest return false; default: return true; } } void ObjectService::OnDisconnected(const DisconnectedEvent&) noexcept { // TODO(cosideci): clear object components } void ObjectService::OnCellChange(const CellChangeEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; PlayerCharacter* pPlayer = PlayerCharacter::Get(); TESObjectCELL* pCell = pPlayer->parentCell; // Player homes should not be synced, so that chest contents, // which are often used as storage, are never accidentally wiped. if (!World::Get().GetServerSettings().SyncPlayerHomes && IsPlayerHome(pCell)) return; GameId cellId{}; if (!m_world.GetModSystem().GetServerModId(pCell->formID, cellId)) { spdlog::error("Server cell id not found for cell form id {:X}", pCell->formID); return; } GameId worldSpaceId{}; if (TESWorldSpace* pWorldSpace = pPlayer->GetWorldSpace()) { if (!m_world.GetModSystem().GetServerModId(pWorldSpace->formID, worldSpaceId)) { spdlog::error("Server world space id not found for world space form id {:X}", pWorldSpace->formID); return; } } Vector formTypes = {FormType::Container, FormType::Door}; // Door seemed to be at the wrong form id (29, now 32), verify this. Vector objects = pCell->GetRefsByFormTypes(formTypes); AssignObjectsRequest request{}; for (TESObjectREFR* pObject : objects) { if (!ShouldSyncObject(pObject)) { spdlog::warn("Excluding sync for {:X}", pObject->formID); continue; } ObjectData objectData{}; objectData.CellId = cellId; objectData.WorldSpaceId = worldSpaceId; objectData.CurrentCoords = GridCellCoords::CalculateGridCellCoords(pObject->position.x, pObject->position.y); if (!m_world.GetModSystem().GetServerModId(pObject->formID, objectData.Id)) { spdlog::error("Server form id not found for object with form id {:X}", pObject->formID); continue; } if (Lock* pLock = pObject->GetLock()) { objectData.CurrentLockData.IsLocked = pLock->IsLocked(); objectData.CurrentLockData.LockLevel = pLock->lockLevel; } if (pObject->baseForm->formType == FormType::Container) objectData.CurrentInventory = pObject->GetInventory(); request.Objects.push_back(objectData); } m_transport.Send(request); } void ObjectService::OnAssignObjectsResponse(const AssignObjectsResponse& acMessage) noexcept { for (const ObjectData& objectData : acMessage.Objects) { const uint32_t cObjectId = World::Get().GetModSystem().GetGameId(objectData.Id); TESObjectREFR* pObject = Cast(TESForm::GetById(cObjectId)); if (!pObject) { spdlog::error("Object not found for form id {:X}", objectData.Id); continue; } CreateObjectEntity(pObject->formID, objectData.ServerId); if (objectData.IsSenderFirst) continue; if (objectData.CurrentLockData != LockData{}) { Lock* pLock = pObject->GetLock(); if (!pLock) { pLock = pObject->CreateLock(); if (!pLock) continue; } pLock->lockLevel = objectData.CurrentLockData.LockLevel; pLock->SetLock(objectData.CurrentLockData.IsLocked); pObject->LockChange(); } if (pObject->baseForm->formType == FormType::Container) { Inventory currentInventory = pObject->GetInventory(); if (currentInventory.ContainsQuestItems()) pObject->SetInventoryRetainingQuestItems(currentInventory, objectData.CurrentInventory); else pObject->SetInventory(objectData.CurrentInventory); } } } entt::entity ObjectService::CreateObjectEntity(const uint32_t acFormId, const uint32_t acServerId) noexcept { const auto view = m_world.view(); auto it = std::find_if(view.begin(), view.end(), [acServerId, view](entt::entity entity) { return view.get(entity).Id == acServerId; }); if (it != view.end()) return *it; entt::entity entity = m_world.create(); spdlog::info("Created object entity, server id: {:X}, form id {:X}", acServerId, acFormId); m_world.emplace(entity, acFormId); m_world.emplace(entity, acServerId); return entity; } void ObjectService::OnActivate(const ActivateEvent& acEvent) noexcept { if (acEvent.ActivateFlag) { acEvent.pObject->Activate(acEvent.pActivator, acEvent.Unk1, acEvent.pObjectToGet, acEvent.Count, acEvent.DefaultProcessing); } if (!m_transport.IsConnected()) return; if (Lock* pLock = acEvent.pObject->GetLock()) { if (pLock->flags & 0xFF) return; } ActivateRequest request; if (!m_world.GetModSystem().GetServerModId(acEvent.pObject->formID, request.Id)) { spdlog::error("Server form id not found for object form id {:X}", acEvent.pObject->formID); return; } TESObjectCELL* pCell = acEvent.pObject->GetParentCellEx(); if (!pCell) { spdlog::error("Activated object has no parent cell: {:X}", acEvent.pObject->formID); return; } if (!m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId)) { spdlog::error("Server cell id not found for cell form id {:X}", acEvent.pObject->parentCell->formID); return; } auto view = m_world.view(); const auto pEntity = std::find_if(std::begin(view), std::end(view), [id = acEvent.pActivator->formID, view](entt::entity entity) { return view.get(entity).Id == id; }); if (pEntity == std::end(view)) { // spdlog::error("Activator entity not found for form id {:X}", acEvent.pActivator->formID); return; } std::optional serverIdRes = Utils::GetServerId(*pEntity); if (!serverIdRes.has_value()) return; request.ActivatorId = serverIdRes.value(); request.PreActivationOpenState = acEvent.PreActivationOpenState; m_transport.Send(request); } void ObjectService::OnActivateNotify(const NotifyActivate& acMessage) noexcept { Actor* pActor = Utils::GetByServerId(acMessage.ActivatorId); if (!pActor) { spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ActivatorId); return; } const uint32_t cObjectId = World::Get().GetModSystem().GetGameId(acMessage.Id); TESObjectREFR* pObject = Cast(TESForm::GetById(cObjectId)); if (!pObject) { spdlog::error("Failed to retrieve object to activate."); return; } if (pObject->baseForm->formType == FormType::Door) { auto remotePreActivationState = static_cast(acMessage.PreActivationOpenState); TESObjectREFR::OpenState localState = pObject->GetOpenState(); if (remotePreActivationState != localState) { // The doors are unsynced at this point. If we'll Activate the one on our side // it'll just continue to be unsynced (open remotely, closed locally and vice versa) return; } } // unsure if these flags are the best, but these are passed with the papyrus Activate fn // might be an idea to have the client send the flags through NotifyActivate pObject->Activate(pActor, 0, nullptr, 1, 0); } void ObjectService::OnLockChange(const LockChangeEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; LockChangeRequest request; if (!m_world.GetModSystem().GetServerModId(acEvent.FormId, request.Id)) { spdlog::error("Server form id for lock object not found, form id: {:X}", acEvent.FormId); return; } const auto* const pObject = Cast(TESForm::GetById(acEvent.FormId)); TESObjectCELL* pCell = pObject->GetParentCellEx(); if (!pCell) { spdlog::error("Activated object has no parent cell: {:X}", pObject->formID); return; } if (!m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId)) { spdlog::error("Server cell id for cell not found, cell form id: {:X}", pObject->parentCell->formID); return; } request.IsLocked = acEvent.IsLocked; request.LockLevel = acEvent.LockLevel; m_transport.Send(request); } void ObjectService::OnLockChangeNotify(const NotifyLockChange& acMessage) noexcept { const auto cObjectId = World::Get().GetModSystem().GetGameId(acMessage.Id); if (cObjectId == 0) { spdlog::error("Failed to retrieve object id to (un)lock."); return; } auto* pObject = Cast(TESForm::GetById(cObjectId)); if (!pObject) { spdlog::error("Failed to retrieve object to (un)lock."); return; } auto* pLock = pObject->GetLock(); if(!acMessage.IsLocked) { if (!pLock || !pLock->IsLocked()) return; } if (!pLock && acMessage.IsLocked) { pLock = pObject->CreateLock(); if (!pLock) { spdlog::error("Failed to create lock for object form id {:X}", pObject->formID); return; } } pLock->lockLevel = acMessage.LockLevel; pLock->SetLock(acMessage.IsLocked); pObject->LockChange(); } void ObjectService::OnScriptAnimationEvent(const ScriptAnimationEvent& acEvent) noexcept { ScriptAnimationRequest request{}; request.FormID = acEvent.FormID; request.Animation = acEvent.Animation; request.EventName = acEvent.EventName; m_transport.Send(request); } void ObjectService::OnNotifyScriptAnimation(const NotifyScriptAnimation& acMessage) noexcept { if (acMessage.FormID == 0) return; auto* pForm = TESForm::GetById(acMessage.FormID); auto* pObject = Cast(pForm); if (!pObject) { spdlog::error("Failed to fetch notify script animation object, form id: {:X}", acMessage.FormID); return; } BSFixedString eventName(acMessage.EventName.c_str()); if (acMessage.Animation == String{}) { pObject->PlayAnimation(&eventName); } else { BSFixedString animation(acMessage.Animation.c_str()); pObject->PlayAnimationAndWait(&animation, &eventName); } } BSTEventResult ObjectService::OnEvent(const TESActivateEvent* acEvent, const EventDispatcher* aDispatcher) { #if ENVIRONMENT_DEBUG auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [id = acEvent->object->formID, view](entt::entity entity) { return view.get(entity).Id == id; }); if (itor == std::end(view)) { AddObjectComponent(acEvent->object); } #endif return BSTEventResult::kOk; }