From b16578e2b92219d178431fcc59913239e9e46d5b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 4 Feb 2026 23:37:30 -0800 Subject: [PATCH] Fix single-player NPC loading outside Goldshire --- include/game/npc_manager.hpp | 1 + src/core/application.cpp | 12 ++++++ src/game/npc_manager.cpp | 75 +++++++++++++++++++++++++----------- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/include/game/npc_manager.hpp b/include/game/npc_manager.hpp index c2e5cfaa..5332fefa 100644 --- a/include/game/npc_manager.hpp +++ b/include/game/npc_manager.hpp @@ -38,6 +38,7 @@ struct NpcInstance { class NpcManager { public: + void clear(rendering::CharacterRenderer* cr, EntityManager* em); void initialize(pipeline::AssetManager* am, rendering::CharacterRenderer* cr, EntityManager& em, diff --git a/src/core/application.cpp b/src/core/application.cpp index 1ed5be37..5797fe70 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -859,6 +859,9 @@ void Application::spawnNpcs() { if (!renderer || !renderer->getCharacterRenderer() || !renderer->getCamera()) return; if (!gameHandler) return; + if (npcManager) { + npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager()); + } npcManager = std::make_unique(); glm::vec3 playerSpawnGL = renderer->getCharacterPosition(); glm::vec3 playerCanonical = core::coords::renderToCanonical(playerSpawnGL); @@ -1138,6 +1141,15 @@ void Application::teleportTo(int presetIndex) { gameHandler->setPosition(finalCanonical.x, finalCanonical.y, finalCanonical.z); } + // Rebuild nearby NPC set for the new location. + if (singlePlayerMode && gameHandler && renderer && renderer->getCharacterRenderer()) { + if (npcManager) { + npcManager->clear(renderer->getCharacterRenderer(), &gameHandler->getEntityManager()); + } + npcsSpawned = false; + spawnNpcs(); + } + LOG_INFO("Teleport to ", preset.label, " complete"); } diff --git a/src/game/npc_manager.cpp b/src/game/npc_manager.cpp index 8c6957d4..756f8e1d 100644 --- a/src/game/npc_manager.cpp +++ b/src/game/npc_manager.cpp @@ -15,10 +15,24 @@ #include #include #include +#include namespace wowee { namespace game { +void NpcManager::clear(rendering::CharacterRenderer* cr, EntityManager* em) { + for (const auto& npc : npcs) { + if (cr) { + cr->removeInstance(npc.renderInstanceId); + } + if (em) { + em->removeEntity(npc.guid); + } + } + npcs.clear(); + loadedModels.clear(); +} + // Random emote animation IDs (humanoid only) static const uint32_t EMOTE_ANIMS[] = { 60, 66, 67, 70 }; // Talk, Bow, Wave, Laugh static constexpr int NUM_EMOTE_ANIMS = 4; @@ -504,16 +518,39 @@ std::vector NpcManager::loadSpawnDefsFromAzerothCoreDb( } } + auto processInsertStatements = + [](std::ifstream& in, const std::function&)>& onTuple) { + std::string line; + std::string stmt; + std::vector tuples; + while (std::getline(in, line)) { + if (stmt.empty()) { + // Skip non-INSERT lines early. + if (line.find("INSERT INTO") == std::string::npos && + line.find("insert into") == std::string::npos) { + continue; + } + } + if (!stmt.empty()) stmt.push_back('\n'); + stmt += line; + if (line.find(';') == std::string::npos) continue; + + if (parseInsertTuples(stmt, tuples)) { + for (const auto& t : tuples) { + if (!onTuple(splitCsvTuple(t))) { + return; + } + } + } + stmt.clear(); + } + }; + // Parse creature_template.sql: entry, modelid1(displayId), name, minlevel. { std::ifstream in(tmplPath); - std::string line; - std::vector tuples; - while (std::getline(in, line)) { - if (!parseInsertTuples(line, tuples)) continue; - for (const auto& t : tuples) { - auto cols = splitCsvTuple(t); - if (cols.size() < 16) continue; + processInsertStatements(in, [&](const std::vector& cols) { + if (cols.size() < 16) return true; try { uint32_t entry = static_cast(std::stoul(cols[0])); uint32_t displayId = static_cast(std::stoul(cols[6])); @@ -531,25 +568,20 @@ std::vector NpcManager::loadSpawnDefsFromAzerothCoreDb( templates[entry] = std::move(tr); } catch (const std::exception&) { } - } - } + return true; + }); } int targetMap = mapNameToId(mapName); constexpr float kRadius = 2200.0f; constexpr size_t kMaxSpawns = 220; std::ifstream in(creaturePath); - std::string line; - std::vector tuples; - while (std::getline(in, line)) { - if (!parseInsertTuples(line, tuples)) continue; - for (const auto& t : tuples) { - auto cols = splitCsvTuple(t); - if (cols.size() < 16) continue; + processInsertStatements(in, [&](const std::vector& cols) { + if (cols.size() < 16) return true; try { uint32_t entry = static_cast(std::stoul(cols[1])); int mapId = static_cast(std::stol(cols[2])); - if (mapId != targetMap) continue; + if (mapId != targetMap) return true; float sx = std::stof(cols[7]); float sy = std::stof(cols[8]); @@ -560,7 +592,7 @@ std::vector NpcManager::loadSpawnDefsFromAzerothCoreDb( glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(sx, sy, sz)); float dx = canonical.x - playerCanonical.x; float dy = canonical.y - playerCanonical.y; - if (dx * dx + dy * dy > kRadius * kRadius) continue; + if (dx * dx + dy * dy > kRadius * kRadius) return true; NpcSpawnDef def; def.mapName = mapName; @@ -584,12 +616,11 @@ std::vector NpcManager::loadSpawnDefsFromAzerothCoreDb( def.scale = 1.0f; def.isCritter = (def.level <= 1 || def.health <= 50); out.push_back(std::move(def)); - if (out.size() >= kMaxSpawns) break; + if (out.size() >= kMaxSpawns) return false; } catch (const std::exception&) { } - } - if (out.size() >= kMaxSpawns) break; - } + return true; + }); LOG_INFO("NpcManager: loaded ", out.size(), " nearby creature spawns from AzerothCore DB at ", basePath); return out;