diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 791895de..4c531f8f 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -60,6 +60,7 @@ struct TerrainTile { // Instance IDs for cleanup on unload std::vector wmoInstanceIds; + std::vector wmoUniqueIds; // For WMO dedup cleanup on unload std::vector m2InstanceIds; std::vector doodadUniqueIds; // For dedup cleanup on unload }; @@ -93,6 +94,7 @@ struct PendingTile { // Pre-loaded WMO data struct WMOReady { uint32_t modelId; + uint32_t uniqueId; pipeline::WMOModel model; glm::vec3 position; glm::vec3 rotation; diff --git a/src/core/application.cpp b/src/core/application.cpp index c595ae57..9b0d2b13 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1659,14 +1659,14 @@ void Application::setupUICallbacks() { transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry); } else { pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; - LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, - " - WMO instance not found (queued move for replay)"); + LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, + " - WMO instance not found yet (queued move for replay)"); return; } } else { pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; - LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, - " - entity not found in EntityManager (queued move for replay)"); + LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, + " - entity not found in EntityManager (queued move for replay)"); return; } } @@ -5137,24 +5137,35 @@ void Application::setupTestTransport() { transportManager->loadPathFromNodes(pathId, harborPath, true, speed); LOG_INFO("Registered transport path ", pathId, " with ", harborPath.size(), " waypoints, speed=", speed); - // Try to load a transport WMO model - // Common transport WMOs: Transportship.wmo (generic ship) - std::string transportWmoPath = "Transports\\Transportship\\Transportship.wmo"; + // Try transport WMOs in manifest-backed paths first. + std::vector transportCandidates = { + "World\\wmo\\transports\\transport_ship\\transportship.wmo", + "World\\wmo\\transports\\transport_zeppelin\\transport_zeppelin.wmo", + "World\\wmo\\transports\\transport_horde_zeppelin\\Transport_Horde_Zeppelin.wmo", + "World\\wmo\\transports\\icebreaker\\Transport_Icebreaker_ship.wmo", + // Legacy fallbacks + "Transports\\Transportship\\Transportship.wmo", + "Transports\\Boat\\Boat.wmo", + }; - auto wmoData = assetManager->readFile(transportWmoPath); - if (wmoData.empty()) { - LOG_WARNING("Could not load transport WMO: ", transportWmoPath); - LOG_INFO("Trying alternative: Boat transport"); - transportWmoPath = "Transports\\Boat\\Boat.wmo"; - wmoData = assetManager->readFile(transportWmoPath); + std::string transportWmoPath; + std::vector wmoData; + for (const auto& candidate : transportCandidates) { + wmoData = assetManager->readFile(candidate); + if (!wmoData.empty()) { + transportWmoPath = candidate; + break; + } } if (wmoData.empty()) { LOG_WARNING("No transport WMO found - test transport disabled"); - LOG_INFO("Available transport WMOs are typically in Transports\\ directory"); + LOG_INFO("Expected under World\\wmo\\transports\\..."); return; } + LOG_INFO("Using transport WMO: ", transportWmoPath); + // Load WMO model pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); LOG_INFO("Transport WMO root loaded: ", transportWmoPath, " nGroups=", wmoModel.nGroups); diff --git a/src/game/entity.cpp b/src/game/entity.cpp index add06327..49b409a8 100644 --- a/src/game/entity.cpp +++ b/src/game/entity.cpp @@ -10,10 +10,11 @@ void EntityManager::addEntity(uint64_t guid, std::shared_ptr entity) { return; } + const int type = static_cast(entity->getType()); entities[guid] = std::move(entity); LOG_DEBUG("Added entity: GUID=0x", std::hex, guid, std::dec, - ", Type=", static_cast(entity->getType())); + ", Type=", type); } void EntityManager::removeEntity(uint64_t guid) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6cde0b74..a0d9a64d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3307,7 +3307,6 @@ void GameHandler::setOrientation(float orientation) { } void GameHandler::handleUpdateObject(network::Packet& packet) { - UpdateObjectData data; if (!packetParsers_->parseUpdateObject(packet, data)) { LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT"); @@ -10588,7 +10587,10 @@ void GameHandler::saveCharacterConfig() { out << "character_guid=" << playerGuid << "\n"; out << "gender=" << static_cast(ch->gender) << "\n"; - out << "use_female_model=" << (ch->useFemaleModel ? 1 : 0) << "\n"; + // For male/female, derive from gender; only nonbinary has a meaningful separate choice + bool saveUseFemaleModel = (ch->gender == Gender::NONBINARY) ? ch->useFemaleModel + : (ch->gender == Gender::FEMALE); + out << "use_female_model=" << (saveUseFemaleModel ? 1 : 0) << "\n"; for (int i = 0; i < ACTION_BAR_SLOTS; i++) { out << "action_bar_" << i << "_type=" << static_cast(actionBar[i].type) << "\n"; out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n"; @@ -10666,8 +10668,14 @@ void GameHandler::loadCharacterConfig() { for (auto& character : characters) { if (character.guid == playerGuid) { character.gender = static_cast(savedGender); - if (savedUseFemaleModel >= 0) { - character.useFemaleModel = (savedUseFemaleModel != 0); + if (character.gender == Gender::NONBINARY) { + // Only nonbinary characters have a meaningful body type choice + if (savedUseFemaleModel >= 0) { + character.useFemaleModel = (savedUseFemaleModel != 0); + } + } else { + // Male/female always use the model matching their gender + character.useFemaleModel = (character.gender == Gender::FEMALE); } LOG_INFO("Applied saved gender: ", getGenderName(character.gender), ", body type: ", (character.useFemaleModel ? "feminine" : "masculine")); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 94bbc444..cafc0e98 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -521,7 +521,10 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { } PendingTile::WMOReady ready; - ready.modelId = placement.uniqueId; + // Cache WMO model uploads by path; placement dedup uses uniqueId separately. + ready.modelId = static_cast(std::hash{}(wmoPath)); + if (ready.modelId == 0) ready.modelId = 1; + ready.uniqueId = placement.uniqueId; ready.model = std::move(wmoModel); ready.position = pos; ready.rotation = rot; @@ -647,10 +650,11 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { std::vector m2InstanceIds; std::vector wmoInstanceIds; std::vector tileUniqueIds; + std::vector tileWmoUniqueIds; // Upload M2 models to GPU and create instances if (m2Renderer && assetManager) { - if (!m2Renderer->getModelCount()) { + if (!m2Renderer->isInitialized()) { m2Renderer->initialize(assetManager); } @@ -691,7 +695,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { // Upload WMO models to GPU and create instances if (wmoRenderer && assetManager) { - if (!wmoRenderer->getModelCount()) { + if (!wmoRenderer->isInitialized()) { wmoRenderer->initialize(assetManager); } @@ -699,9 +703,9 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { int loadedLiquids = 0; int skippedWmoDedup = 0; for (auto& wmoReady : pending->wmoModels) { - // Deduplicate WMO instances by uniqueId (prevents Stormwind from rendering 16x) - // uniqueId is stored in modelId field (see line 522 in prepareTile) - if (placedWmoIds.count(wmoReady.modelId)) { + // Deduplicate by placement uniqueId when available. + // Some ADTs use uniqueId=0, which is not safe for dedup. + if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { skippedWmoDedup++; continue; } @@ -710,7 +714,10 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); if (wmoInstId) { wmoInstanceIds.push_back(wmoInstId); - placedWmoIds.insert(wmoReady.modelId); + if (wmoReady.uniqueId != 0) { + placedWmoIds.insert(wmoReady.uniqueId); + tileWmoUniqueIds.push_back(wmoReady.uniqueId); + } loadedWMOs++; // Load WMO liquids (canals, pools, etc.) @@ -773,6 +780,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { tile->loaded = true; tile->m2InstanceIds = std::move(m2InstanceIds); tile->wmoInstanceIds = std::move(wmoInstanceIds); + tile->wmoUniqueIds = std::move(tileWmoUniqueIds); tile->doodadUniqueIds = std::move(tileUniqueIds); // Calculate world bounds @@ -1018,6 +1026,9 @@ void TerrainManager::unloadTile(int x, int y) { for (uint32_t uid : tile->doodadUniqueIds) { placedDoodadIds.erase(uid); } + for (uint32_t uid : tile->wmoUniqueIds) { + placedWmoIds.erase(uid); + } // Remove M2 doodad instances if (m2Renderer) {