#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DiscoveryService::DiscoveryService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) , m_dispatcher(aDispatcher) { m_preUpdateConnection = m_dispatcher.sink().connect<&DiscoveryService::OnUpdate>(this); m_connectedConnection = m_dispatcher.sink().connect<&DiscoveryService::OnConnected>(this); EventDispatcherManager::Get()->loadGameEvent.RegisterSink(this); } void DiscoveryService::VisitCell(bool aForceTrigger) noexcept { const PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (!pPlayer) return; if (pPlayer->GetWorldSpace()) VisitExteriorCell(aForceTrigger); else if (pPlayer->GetParentCell()) VisitInteriorCell(aForceTrigger); // exactly how the game does it too if (m_pLocation != pPlayer->locationForm) { m_dispatcher.trigger(LocationChangeEvent()); m_pLocation = pPlayer->locationForm; } } void DiscoveryService::VisitExteriorCell(bool aForceTrigger) noexcept { const PlayerCharacter* pPlayer = PlayerCharacter::Get(); const auto pWorldSpace = pPlayer->GetWorldSpace(); m_interiorCellId = 0; const TES* pTES = TES::Get(); const uint32_t worldSpaceId = pWorldSpace->formID; const GridCellCoords gameCurrentGrid(pTES->currentGridX, pTES->currentGridY); const GridCellCoords gameCenterGrid(pTES->centerGridX, pTES->centerGridY); if (m_worldSpaceId != worldSpaceId || aForceTrigger) { DetectGridCellChange(pWorldSpace, true); // If the world space changes, then we want to send out a CellChangeEvent out too. aForceTrigger = true; } else if (gameCenterGrid != m_centerGrid) { DetectGridCellChange(pWorldSpace, false); } if (gameCurrentGrid != m_currentGrid || aForceTrigger) { CellChangeEvent cellChangeEvent{}; if (!m_world.GetModSystem().GetServerModId(pWorldSpace->formID, cellChangeEvent.WorldSpaceId)) { spdlog::error("Failed to find world space id for form id {:X}", pWorldSpace->formID); return; } TESObjectCELL* pCell = pPlayer->GetParentCellEx(); if (!pCell) pCell = ModManager::Get()->GetCellFromCoordinates(gameCurrentGrid.X, gameCurrentGrid.Y, pWorldSpace, false); if (!m_world.GetModSystem().GetServerModId(pCell->formID, cellChangeEvent.CellId)) { spdlog::error("Failed to find cell id for form id {:X}", pCell->formID); return; } cellChangeEvent.CurrentCoords = gameCurrentGrid; m_dispatcher.trigger(cellChangeEvent); m_currentGrid = gameCurrentGrid; } } void DiscoveryService::VisitInteriorCell(bool aForceTrigger) noexcept { ResetCachedCellData(); const uint32_t cellId = PlayerCharacter::Get()->GetParentCell()->formID; if (m_interiorCellId != cellId || aForceTrigger) { CellChangeEvent cellChangeEvent{}; if (!m_world.GetModSystem().GetServerModId(cellId, cellChangeEvent.CellId)) { spdlog::error("Failed to find cell id {:X}", cellId); return; } m_dispatcher.trigger(cellChangeEvent); m_interiorCellId = cellId; } } void DiscoveryService::DetectGridCellChange(TESWorldSpace* aWorldSpace, bool aNewCellGrid) noexcept { GridCellChangeEvent changeEvent{}; const uint32_t worldSpaceId = aWorldSpace->formID; changeEvent.WorldSpaceId = worldSpaceId; const TES* pTES = TES::Get(); const int32_t startGridX = pTES->centerGridX - GridCellCoords::m_gridsToLoad / 2; const int32_t startGridY = pTES->centerGridY - GridCellCoords::m_gridsToLoad / 2; for (int32_t i = 0; i < GridCellCoords::m_gridsToLoad; ++i) { for (int32_t j = 0; j < GridCellCoords::m_gridsToLoad; ++j) { // If it is a new cell grid, don't check for previously loaded cells. if (!aNewCellGrid) { if (GridCellCoords::IsCellInGridCell(m_centerGrid, {startGridX + i, startGridY + j}, false)) continue; } const TESObjectCELL* pCell = ModManager::Get()->GetCellFromCoordinates(startGridX + i, startGridY + j, aWorldSpace, 0); if (!pCell) { spdlog::warn("Cell not found at coordinates ({}, {}) in worldspace {:X}", startGridX + i, startGridY + j, aWorldSpace->formID); continue; } GameId cellId{}; if (!m_world.GetModSystem().GetServerModId(pCell->formID, cellId)) { spdlog::error("Failed to find cell id for form id {:X}", pCell->formID); continue; } changeEvent.Cells.push_back(cellId); } } TESObjectCELL* pCell = PlayerCharacter::Get()->GetParentCellEx(); if (!pCell) pCell = ModManager::Get()->GetCellFromCoordinates(pTES->currentGridX, pTES->currentGridY, aWorldSpace, false); if (!m_world.GetModSystem().GetServerModId(pCell->formID, changeEvent.PlayerCell)) { spdlog::error("Failed to find cell id for form id {:X}", pCell->formID); return; } changeEvent.CenterCoords = m_centerGrid = {pTES->centerGridX, pTES->centerGridY}; m_dispatcher.trigger(changeEvent); m_worldSpaceId = worldSpaceId; } void DiscoveryService::VisitForms() noexcept { static Set s_previousForms; s_previousForms = m_forms; const auto visitor = [this](TESObjectREFR* apReference) { const auto formId = apReference->formID; if (!m_forms.count(formId)) { m_forms.insert(formId); m_dispatcher.enqueue(ActorAddedEvent(formId)); } else s_previousForms.erase(formId); }; ProcessLists* const pProcessLists = ProcessLists::Get(); if (!pProcessLists) return; for (uint32_t i = 0; i < pProcessLists->highActorHandleArray.length; ++i) { TESObjectREFR* const pRefr = TESObjectREFR::GetByHandle(pProcessLists->highActorHandleArray[i]); if (pRefr) { if (pRefr->GetNiNode()) { visitor(pRefr); } } } // Not in actor holder visitor(PlayerCharacter::Get()); // We dispatch removal events first to prevent needless reallocations for (uint32_t formId : s_previousForms) { m_dispatcher.trigger(ActorRemovedEvent(formId)); m_forms.erase(formId); } // Dispatch all adds m_dispatcher.update(); } void DiscoveryService::OnUpdate(const PreUpdateEvent& acUpdateEvent) noexcept { TP_UNUSED(acUpdateEvent); VisitCell(); VisitForms(); } void DiscoveryService::OnConnected(const ConnectedEvent& acEvent) noexcept { // uGridsToLoad should always be 5, as this is what the server enforces auto* pSetting = INISettingCollection::Get()->GetSetting("uGridsToLoad:General"); if (pSetting && pSetting->data != 5) { ConnectionErrorEvent errorEvent{}; errorEvent.ErrorDetail = "{\"error\": \"bad_uGridsToLoad\"}"; m_world.GetRunner().Trigger(errorEvent); } VisitCell(true); } BSTEventResult DiscoveryService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) { spdlog::info("Finished loading, triggering visit cell"); const TiltedPhoques::String defaultModlist[7] = {"Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm", "_ResourcePack.esl", "SkyrimTogether.esp"}; auto& currentModlist = ModManager::Get()->mods; bool isModlistEqual = currentModlist.Size() == 7; if (isModlistEqual) { int i = 0; for (const auto& currentMod : currentModlist) { if (currentMod->filename != defaultModlist[i]) { isModlistEqual = false; break; } i++; } } if (!isModlistEqual) { ConnectionErrorEvent errorEvent{}; errorEvent.ErrorDetail = "{\"error\": \"non_default_install\"}"; m_world.GetRunner().Trigger(errorEvent); } VisitCell(true); return BSTEventResult::kOk; } void DiscoveryService::ResetCachedCellData() noexcept { m_worldSpaceId = 0; m_centerGrid.Reset(); m_currentGrid.Reset(); }