/* * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "DynamicMMapTileBuilder.h" #include "Containers.h" #include "DB2Stores.h" #include "DeadlineTimer.h" #include "GameObjectModel.h" #include "GameTime.h" #include "Hash.h" #include "IoContext.h" #include "Log.h" #include "MMapDefines.h" #include "MMapManager.h" #include "Map.h" #include "VMapFactory.h" #include "VMapManager.h" #include "World.h" #include "advstd.h" #include namespace { struct TileCacheKeyObject { uint32 DisplayId; int16 Scale; std::array Position; int64 Rotation; friend std::strong_ordering operator<=>(TileCacheKeyObject const&, TileCacheKeyObject const&) = default; friend bool operator==(TileCacheKeyObject const&, TileCacheKeyObject const&) = default; }; struct TileCacheKey { uint32 TerrainMapId; uint32 X; uint32 Y; std::size_t CachedHash; // computing the hash is expensive - store it std::vector Objects; friend bool operator==(TileCacheKey const&, TileCacheKey const&) = default; }; } template <> struct std::hash { static std::size_t Compute(TileCacheKey const& key) noexcept { size_t hashVal = 0; Trinity::hash_combine(hashVal, key.TerrainMapId); Trinity::hash_combine(hashVal, key.X); Trinity::hash_combine(hashVal, key.Y); for (TileCacheKeyObject const& object : key.Objects) { Trinity::hash_combine(hashVal, object.DisplayId); Trinity::hash_combine(hashVal, object.Scale); Trinity::hash_combine(hashVal, object.Position[0]); Trinity::hash_combine(hashVal, object.Position[1]); Trinity::hash_combine(hashVal, object.Position[2]); Trinity::hash_combine(hashVal, object.Rotation); } return hashVal; } std::size_t operator()(TileCacheKey const& key) const noexcept { return key.CachedHash; } }; namespace { std::unique_ptr CreateVMapManager(uint32 mapId) { std::unique_ptr vmgr = std::make_unique(); do { MapEntry const* mapEntry = sMapStore.AssertEntry(mapId); if (!mapEntry) break; vmgr->InitializeThreadUnsafe(mapId, mapEntry->ParentMapID); if (mapEntry->ParentMapID < 0) break; mapId = mapEntry->ParentMapID; } while (true); VMAP::VMapManager* globalManager = VMAP::VMapFactory::createOrGetVMapManager(); vmgr->GetLiquidFlagsPtr = globalManager->GetLiquidFlagsPtr; vmgr->IsVMAPDisabledForPtr = globalManager->IsVMAPDisabledForPtr; vmgr->LoadPathOnlyModels = globalManager->LoadPathOnlyModels; return vmgr; } struct TileCache { static TileCache* Instance() { static TileCache tc; return &tc; } struct Tile { std::shared_ptr Data; TimePoint LastAccessed; }; static constexpr TimePoint::duration CACHE_CLEANUP_INTERVAL = 5min; static constexpr TimePoint::duration CACHE_MAX_AGE = 30min; std::mutex TilesMutex; std::unordered_map Tiles; TileCache() : _taskContext(1), _cacheCleanupTimer(_taskContext, CACHE_CLEANUP_INTERVAL) { MMAP::CreateVMapManager = &CreateVMapManager; // init timer OnCacheCleanupTimerTick({}); // start the worker _builderThread = std::thread([this] { _taskContext.run(); }); } TileCache(TileCache const&) = delete; TileCache(TileCache&&) = delete; TileCache& operator=(TileCache const&) = delete; TileCache& operator=(TileCache&&) = delete; ~TileCache() { _cacheCleanupTimer.cancel(); _builderThread.join(); } template auto StartTask(Task&& task) { return Trinity::Asio::post(_taskContext, std::forward(task)); } private: void OnCacheCleanupTimerTick(boost::system::error_code const& error) { if (error) return; TimePoint now = GameTime::Now(); RemoveOldCacheEntries(now - CACHE_MAX_AGE); _cacheCleanupTimer.expires_at(now + CACHE_CLEANUP_INTERVAL); _cacheCleanupTimer.async_wait([this](boost::system::error_code const& error) { OnCacheCleanupTimerTick(error); }); } void RemoveOldCacheEntries(TimePoint oldestPreservedEntryTimestamp) { std::lock_guard lock(TilesMutex); Trinity::Containers::EraseIf(Tiles, [=](std::unordered_map::value_type const& kv) { return kv.second.LastAccessed < oldestPreservedEntryTimestamp; }); } Trinity::Asio::IoContext _taskContext; Trinity::Asio::DeadlineTimer _cacheCleanupTimer; std::thread _builderThread; }; } namespace MMAP { struct DynamicTileBuilder::TileId { uint32 TerrainMapId; uint32 X; uint32 Y; friend bool operator==(TileId const&, TileId const&) = default; }; struct TileBuildRequest { DynamicTileBuilder::TileId Id; std::weak_ptr Result; dtNavMesh* NavMesh; }; bool InvokeAsyncCallbackIfReady(TileBuildRequest& request) { std::shared_ptr result = request.Result.lock(); if (!result) return true; // expired, mark as complete and do nothing if (!result->IsReady.load(std::memory_order::acquire)) return false; TileBuilder::TileResult const& tileResult = result->Result; if (tileResult.data) { dtMeshHeader const* header = reinterpret_cast(tileResult.data.get()); if (dtTileRef tileRef = request.NavMesh->getTileRefAt(header->x, header->y, 0)) { TC_LOG_INFO("maps.mmapgen", "[Map {:04}] [{:02},{:02}]: Swapping new tile", request.Id.TerrainMapId, request.Id.Y, request.Id.X); request.NavMesh->removeTile(tileRef, nullptr, nullptr); unsigned char* data = static_cast(dtAlloc(tileResult.size, DT_ALLOC_PERM)); std::memcpy(data, tileResult.data.get(), tileResult.size); request.NavMesh->addTile(data, tileResult.size, DT_TILE_FREE_DATA, tileRef, nullptr); } } return true; } static constexpr auto SetAsyncCallbackReady = [](DynamicTileBuilder::AsyncTileResult* result) { result->IsReady.store(true, std::memory_order::release); }; DynamicTileBuilder::DynamicTileBuilder(Map* map, dtNavMesh* navMesh) : TileBuilder(sWorld->GetDataPath(), sWorld->GetDataPath(), {}, {}, false, false, false, nullptr), m_map(map), m_navMesh(navMesh), m_rebuildCheckTimer(1s) { } DynamicTileBuilder::~DynamicTileBuilder() = default; void DynamicTileBuilder::AddTile(uint32 terrainMapId, uint32 tileX, uint32 tileY) { TileId id = { .TerrainMapId = terrainMapId, .X = tileX, .Y = tileY }; if (!advstd::ranges::contains(m_tilesToRebuild, id)) m_tilesToRebuild.push_back(id); } void DynamicTileBuilder::Update(Milliseconds diff) { m_rebuildCheckTimer.Update(diff); if (m_rebuildCheckTimer.Passed()) { for (TileId const& tileId : m_tilesToRebuild) m_tiles.AddCallback({ .Id = tileId, .Result = BuildTile(tileId.TerrainMapId, tileId.X, tileId.Y), .NavMesh = m_navMesh }); m_tilesToRebuild.clear(); m_rebuildCheckTimer.Reset(1s); } m_tiles.ProcessReadyCallbacks(); } std::weak_ptr DynamicTileBuilder::BuildTile(uint32 terrainMapId, uint32 tileX, uint32 tileY) { std::vector> gameObjectModelReferences; // hold strong refs to models for (GameObjectModel const* gameObjectModel : m_map->GetGameObjectModelsInGrid(tileX, tileY)) { if (!gameObjectModel->IsMapObject() || !gameObjectModel->IsIncludedInNavMesh()) continue; std::shared_ptr worldModel = gameObjectModel->GetWorldModel(); if (!worldModel) continue; gameObjectModelReferences.emplace_back(worldModel, gameObjectModel); } TileCacheKey cacheKey{ .TerrainMapId = terrainMapId, .X = tileX, .Y = tileY, .CachedHash = 0, .Objects = std::vector(gameObjectModelReferences.size()) }; for (std::size_t i = 0; i < gameObjectModelReferences.size(); ++i) { GameObjectModel const* gameObjectModel = gameObjectModelReferences[i].get(); TileCacheKeyObject& object = cacheKey.Objects[i]; object.DisplayId = gameObjectModel->GetDisplayId(); object.Scale = int16(gameObjectModel->GetScale() * 1024.0f); object.Position = [](G3D::Vector3 const& pos) -> std::array { return { int16(pos.x), int16(pos.y), int16(pos.z) }; }(gameObjectModel->GetPosition()); object.Rotation = gameObjectModel->GetPackedRotation(); } // Ensure spawn order is stable after adding/removing gameobjects from the map for hash calculation std::ranges::sort(cacheKey.Objects); cacheKey.CachedHash = std::hash::Compute(cacheKey); TileCache* tileCache = TileCache::Instance(); std::lock_guard lock(tileCache->TilesMutex); auto [itr, isNew] = tileCache->Tiles.try_emplace(std::move(cacheKey)); itr->second.LastAccessed = GameTime::Now(); if (!isNew) return itr->second.Data; itr->second.Data = std::make_shared(); tileCache->StartTask([result = itr->second.Data, hash = cacheKey.CachedHash, selfRef = weak_from_this(), terrainMapId, tileX, tileY, gameObjectModelReferences = std::move(gameObjectModelReferences)]() mutable { auto isReadyGuard = Trinity::make_unique_ptr_with_deleter(result.get()); std::shared_ptr self = selfRef.lock(); if (!self) return; // get navmesh params dtNavMeshParams params; std::vector offMeshConnections; if (MMapManager::parseNavMeshParamsFile(sWorld->GetDataPath(), terrainMapId, ¶ms, &offMeshConnections) != LoadResult::Success) return; std::unique_ptr vmapManager = CreateVMapManager(terrainMapId); MeshData meshData; // get heightmap data self->m_terrainBuilder.loadMap(terrainMapId, tileX, tileY, meshData, vmapManager.get()); // get model data self->m_terrainBuilder.loadVMap(terrainMapId, tileX, tileY, meshData, vmapManager.get()); for (std::shared_ptr const& gameObjectModel : gameObjectModelReferences) { G3D::Vector3 position = gameObjectModel->GetPosition(); position.x = -position.x; position.y = -position.y; G3D::Matrix3 invRotation = (G3D::Quat(0, 0, 1, 0) * gameObjectModel->GetRotation()).toRotationMatrix().inverse(); self->m_terrainBuilder.loadVMapModel(gameObjectModel->GetWorldModel().get(), position, invRotation, gameObjectModel->GetScale(), meshData, vmapManager.get()); } // if there is no data, give up now if (meshData.solidVerts.empty() && meshData.liquidVerts.empty()) return; // remove unused vertices TerrainBuilder::cleanVertices(meshData.solidVerts, meshData.solidTris); TerrainBuilder::cleanVertices(meshData.liquidVerts, meshData.liquidTris); // gather all mesh data for final data check, and bounds calculation std::vector allVerts(meshData.liquidVerts.size() + meshData.solidVerts.size()); std::ranges::copy(meshData.liquidVerts, allVerts.begin()); std::ranges::copy(meshData.solidVerts, allVerts.begin() + std::ssize(meshData.liquidVerts)); // get bounds of current tile float bmin[3], bmax[3]; getTileBounds(tileX, tileY, allVerts.data(), allVerts.size() / 3, bmin, bmax); self->m_terrainBuilder.loadOffMeshConnections(terrainMapId, tileX, tileY, meshData, offMeshConnections); // build navmesh tile std::string debugSuffix = Trinity::StringFormat("_{:016X}", hash); result->Result = self->buildMoveMapTile(terrainMapId, tileX, tileY, meshData, bmin, bmax, ¶ms); if (self->m_debugOutput && result->Result.data) self->saveMoveMapTileToFile(terrainMapId, tileX, tileY, nullptr, result->Result, debugSuffix); }); return itr->second.Data; } }