From 8f0d2cc4abffdcbf57e26d22b8530cae573b9f90 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 21:57:42 -0700 Subject: [PATCH] terrain: pre-load bind point tiles during Hearthstone cast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the player starts casting Hearthstone (spell IDs 6948/8690), trigger background terrain loading at the bind point so tiles are ready when the teleport fires. - Add HearthstonePreloadCallback to GameHandler, called from handleSpellStart when a Hearthstone cast begins. - Application callback enqueues a 5×5 tile grid around the bind point via precacheTiles() (same-map) or starts a file-cache warm via startWorldPreload() (cross-map) during the ~10 s cast time. - On same-map teleport arrival, call processAllReadyTiles() to GPU-upload any tiles that finished parsing during the cast before the first frame at the new position. Fixes: player landing in unloaded terrain and falling after Hearthstone. --- include/game/game_handler.hpp | 7 +++++ src/core/application.cpp | 49 +++++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 8 ++++++ 3 files changed, 64 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7ce933bf..8053fb42 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -610,6 +610,12 @@ public: using BindPointCallback = std::function; void setBindPointCallback(BindPointCallback cb) { bindPointCallback_ = std::move(cb); } + // Called when the player starts casting Hearthstone so terrain at the bind + // point can be pre-loaded during the cast time. + // Parameters: mapId and canonical (x, y, z) of the bind location. + using HearthstonePreloadCallback = std::function; + void setHearthstonePreloadCallback(HearthstonePreloadCallback cb) { hearthstonePreloadCallback_ = std::move(cb); } + // Creature spawn callback (online mode - triggered when creature enters view) // Parameters: guid, displayId, x, y, z (canonical), orientation using CreatureSpawnCallback = std::function; @@ -1683,6 +1689,7 @@ private: UnstuckCallback unstuckGyCallback_; UnstuckCallback unstuckHearthCallback_; BindPointCallback bindPointCallback_; + HearthstonePreloadCallback hearthstonePreloadCallback_; CreatureSpawnCallback creatureSpawnCallback_; CreatureDespawnCallback creatureDespawnCallback_; PlayerSpawnCallback playerSpawnCallback_; diff --git a/src/core/application.cpp b/src/core/application.cpp index f26e9865..b3883e0c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1710,6 +1710,10 @@ void Application::setupUICallbacks() { renderer->getCameraController()->clearMovementInputs(); renderer->getCameraController()->suppressMovementFor(0.5f); } + // Flush any tiles that finished background parsing during the cast + // (e.g. Hearthstone pre-loaded them) so they're GPU-uploaded before + // the first frame at the new position. + renderer->getTerrainManager()->processAllReadyTiles(); return; } @@ -1950,6 +1954,51 @@ void Application::setupUICallbacks() { LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"); }); + // Hearthstone preload callback: begin loading terrain at the bind point as soon as + // the player starts casting Hearthstone. The ~10 s cast gives enough time for + // the background streaming workers to bring tiles into the cache so the player + // lands on solid ground instead of falling through un-loaded terrain. + gameHandler->setHearthstonePreloadCallback([this](uint32_t mapId, float x, float y, float z) { + if (!renderer || !assetManager) return; + + auto* terrainMgr = renderer->getTerrainManager(); + if (!terrainMgr) return; + + // Resolve map name from the cached Map.dbc table + std::string mapName; + if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) { + mapName = it->second; + } else { + mapName = mapIdToName(mapId); + } + if (mapName.empty()) mapName = "Azeroth"; + + if (mapId == loadedMapId_) { + // Same map: pre-enqueue tiles around the bind point so workers start + // loading them now. Uses render-space coords (canonicalToRender). + glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); + auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y); + + std::vector> tiles; + tiles.reserve(25); + for (int dy = -2; dy <= 2; dy++) + for (int dx = -2; dx <= 2; dx++) + tiles.push_back({tileX + dx, tileY + dy}); + + terrainMgr->precacheTiles(tiles); + LOG_INFO("Hearthstone preload: enqueued ", tiles.size(), + " tiles around bind point (same map) tile=[", tileX, ",", tileY, "]"); + } else { + // Different map: warm the file cache so ADT parsing is fast when + // loadOnlineWorldTerrain runs its blocking load loop. + // homeBindPos_ is canonical; startWorldPreload expects server coords. + glm::vec3 server = core::coords::canonicalToServer(glm::vec3(x, y, z)); + startWorldPreload(mapId, mapName, server.x, server.y); + LOG_INFO("Hearthstone preload: started file cache warm for map '", mapName, + "' (id=", mapId, ")"); + } + }); + // Faction hostility map is built in buildFactionHostilityMap() when character enters world // Creature spawn callback (online mode) - spawn creature models diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 37c6591f..a34ae7bf 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12430,6 +12430,14 @@ void GameHandler::handleSpellStart(network::Packet& packet) { ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); } } + + // Hearthstone cast: begin pre-loading terrain at bind point during cast time + // so tiles are ready when the teleport fires (avoids falling through un-loaded terrain). + // Spell IDs: 6948 = Vanilla Hearthstone (rank 1), 8690 = TBC/WotLK Hearthstone + const bool isHearthstone = (data.spellId == 6948 || data.spellId == 8690); + if (isHearthstone && hasHomeBind_ && hearthstonePreloadCallback_) { + hearthstonePreloadCallback_(homeBindMapId_, homeBindPos_.x, homeBindPos_.y, homeBindPos_.z); + } } }