diff --git a/include/core/application.hpp b/include/core/application.hpp index e8a3db54..9101a844 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -303,7 +303,7 @@ private: float orientation = 0.0f; }; std::vector pendingTransportDoodadBatches_; - static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 12; + static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4; void processPendingTransportDoodads(); // Quest marker billboard sprites (above NPCs) diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index b5b4f518..30001846 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -216,6 +216,7 @@ public: /** Load a BLP texture from MPQ and return the GL texture ID (cached). */ GLuint loadTexture(const std::string& path); + GLuint getTransparentTexture() const { return transparentTexture; } /** Replace a loaded model's texture at the given slot with a new GL texture. */ void setModelTexture(uint32_t modelId, uint32_t textureSlot, GLuint textureId); @@ -261,6 +262,7 @@ private: uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; // Default, overridden at init GLuint whiteTexture = 0; + GLuint transparentTexture = 0; std::unordered_map models; std::unordered_map instances; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 9ba73f2b..dc14c4fd 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -176,6 +176,8 @@ public: LightingManager* getLightingManager() { return lightingManager.get(); } private: + void runDeferredWorldInitStep(float deltaTime); + core::Window* window = nullptr; std::unique_ptr camera; std::unique_ptr cameraController; @@ -259,6 +261,10 @@ private: bool inTavern_ = false; bool inBlacksmith_ = false; float musicSwitchCooldown_ = 0.0f; + bool deferredWorldInitEnabled_ = true; + bool deferredWorldInitPending_ = false; + uint8_t deferredWorldInitStage_ = 0; + float deferredWorldInitCooldown_ = 0.0f; // Third-person character state glm::vec3 characterPosition = glm::vec3(0.0f); diff --git a/src/core/application.cpp b/src/core/application.cpp index d1c59669..325282a8 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -774,7 +774,7 @@ void Application::update(float deltaTime) { // Debug: Log transport state changes static bool wasOnTransport = false; if (onTransport != wasOnTransport) { - LOG_INFO("Transport state changed: onTransport=", onTransport, + LOG_DEBUG("Transport state changed: onTransport=", onTransport, " guid=0x", std::hex, gameHandler->getPlayerTransportGuid(), std::dec); wasOnTransport = onTransport; } @@ -1721,7 +1721,7 @@ void Application::setupUICallbacks() { } uint32_t wmoInstanceId = it->second.instanceId; - LOG_INFO("Registering server transport: GUID=0x", std::hex, guid, std::dec, + LOG_DEBUG("Registering server transport: GUID=0x", std::hex, guid, std::dec, " entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId, " pos=(", x, ", ", y, ", ", z, ")"); @@ -1730,7 +1730,7 @@ void Application::setupUICallbacks() { const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid); bool clientAnim = transportManager->isClientSideAnimation(); - LOG_INFO("Transport spawn callback: clientAnimation=", clientAnim, + LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim, " guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId, " preferServer=", preferServerData); @@ -1754,10 +1754,10 @@ void Application::setupUICallbacks() { if (!hasUsablePath) { std::vector path = { canonicalSpawnPos }; transportManager->loadPathFromNodes(pathId, path, false, 0.0f); - LOG_INFO("Server-first strict registration: stationary fallback for GUID 0x", + LOG_DEBUG("Server-first strict registration: stationary fallback for GUID 0x", std::hex, guid, std::dec, " entry=", entry); } else { - LOG_INFO("Server-first transport registration: using entry DBC path for entry ", entry); + LOG_DEBUG("Server-first transport registration: using entry DBC path for entry ", entry); } } else if (!hasUsablePath) { // Remap/infer path by spawn position when entry doesn't map 1:1 to DBC ids. @@ -1767,12 +1767,12 @@ void Application::setupUICallbacks() { canonicalSpawnPos, 1200.0f, allowZOnly); if (inferredPath != 0) { pathId = inferredPath; - LOG_INFO("Using inferred transport path ", pathId, " for entry ", entry); + LOG_DEBUG("Using inferred transport path ", pathId, " for entry ", entry); } else { uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId); if (remappedPath != 0) { pathId = remappedPath; - LOG_INFO("Using remapped fallback transport path ", pathId, + LOG_DEBUG("Using remapped fallback transport path ", pathId, " for entry ", entry, " displayId=", displayId, " (usableEntryPath=", transportManager->hasPathForEntry(entry), ")"); } else { @@ -1785,7 +1785,7 @@ void Application::setupUICallbacks() { } } } else { - LOG_INFO("Using real transport path from TransportAnimation.dbc for entry ", entry); + LOG_DEBUG("Using real transport path from TransportAnimation.dbc for entry ", entry); } // Register the transport with spawn position (prevents rendering at origin until server update) @@ -1800,7 +1800,7 @@ void Application::setupUICallbacks() { if (pendingIt != pendingTransportMoves_.end()) { const PendingTransportMove pending = pendingIt->second; transportManager->updateServerTransport(guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation); - LOG_INFO("Replayed queued transport move for GUID=0x", std::hex, guid, std::dec, + LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, guid, std::dec, " pos=(", pending.x, ", ", pending.y, ", ", pending.z, ") orientation=", pending.orientation); pendingTransportMoves_.erase(pendingIt); } @@ -1812,27 +1812,27 @@ void Application::setupUICallbacks() { uint32_t taxiPathId = goData->data[0]; if (transportManager->hasTaxiPath(taxiPathId)) { transportManager->assignTaxiPathToTransport(entry, taxiPathId); - LOG_INFO("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry, + LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry, " taxiPathId=", taxiPathId); } } } if (auto* tr = transportManager->getTransport(guid); tr) { - LOG_INFO("Transport registered: guid=0x", std::hex, guid, std::dec, + LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec, " entry=", entry, " displayId=", displayId, " pathId=", tr->pathId, " mode=", (tr->useClientAnimation ? "client" : "server"), " serverUpdates=", tr->serverUpdateCount); } else { - LOG_INFO("Transport registered: guid=0x", std::hex, guid, std::dec, + LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec, " entry=", entry, " displayId=", displayId, " (TransportManager instance missing)"); } }); // Transport move callback (online mode) - update transport gameobject positions gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float orientation) { - LOG_INFO("Transport move callback: GUID=0x", std::hex, guid, std::dec, + LOG_DEBUG("Transport move callback: GUID=0x", std::hex, guid, std::dec, " pos=(", x, ", ", y, ", ", z, ") orientation=", orientation); auto* transportManager = gameHandler->getTransportManager(); @@ -1843,7 +1843,7 @@ void Application::setupUICallbacks() { // Check if transport exists - if not, treat this as a late spawn (reconnection/server restart) if (!transportManager->getTransport(guid)) { - LOG_INFO("Received position update for unregistered transport 0x", std::hex, guid, std::dec, + LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec, " - auto-spawning from position update"); // Get transport info from entity manager @@ -3574,6 +3574,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Build equipment texture region layers from NPC equipment display IDs // (texture-only compositing — no geoset changes to avoid invisibility bugs) std::vector> npcRegionLayers; + std::string npcCapeTexturePath; auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); if (npcItemDisplayDbc) { static const char* npcComponentDirs[] = { @@ -3597,7 +3598,31 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x }; const bool npcIsFemale = (extra.sexId == 1); - // Iterate all 11 NPC equipment slots; let DBC lookup filter which have textures + auto regionAllowedForNpcSlot = [](int eqSlot, int region) -> bool { + // Regions: 0 ArmUpper, 1 ArmLower, 2 Hand, 3 TorsoUpper, 4 TorsoLower, + // 5 LegUpper, 6 LegLower, 7 Foot + switch (eqSlot) { + case 2: // shirt + case 3: // chest + return region <= 4; + case 4: // belt + return region == 4; + case 5: // legs + return region == 5 || region == 6; + case 6: // feet + return region == 7; + case 7: // wrist + return region == 1; + case 8: // hands + return region == 0 || region == 1 || region == 2; + case 9: // tabard + return region == 3 || region == 4; + default: + return false; + } + }; + + // Iterate all 11 NPC equipment slots; use slot-aware region filtering for (int eqSlot = 0; eqSlot < 11; eqSlot++) { uint32_t did = extra.equipDisplayId[eqSlot]; if (did == 0) continue; @@ -3605,6 +3630,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (recIdx < 0) continue; for (int region = 0; region < 8; region++) { + if (!regionAllowedForNpcSlot(eqSlot, region)) continue; std::string texName = npcItemDisplayDbc->getString( static_cast(recIdx), texRegionFields[region]); if (texName.empty()) continue; @@ -3621,6 +3647,77 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x npcRegionLayers.emplace_back(region, fullPath); } } + + // Cloak/cape texture is separate from the body atlas. + // Read equipped cape displayId (slot 10) and resolve the best cape texture path. + uint32_t capeDisplayId = extra.equipDisplayId[10]; + if (capeDisplayId != 0) { + int32_t capeRecIdx = npcItemDisplayDbc->findRecordById(capeDisplayId); + if (capeRecIdx >= 0) { + const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u; + const uint32_t rightTexField = leftTexField + 1u; // modelTexture_2 in 3.3.5a + + std::vector capeNames; + auto addName = [&](const std::string& n) { + if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) { + capeNames.push_back(n); + } + }; + std::string leftName = npcItemDisplayDbc->getString( + static_cast(capeRecIdx), leftTexField); + std::string rightName = npcItemDisplayDbc->getString( + static_cast(capeRecIdx), rightTexField); + // Female models often prefer modelTexture_2. + if (npcIsFemale) { + addName(rightName); + addName(leftName); + } else { + addName(leftName); + addName(rightName); + } + + auto hasBlpExt = [](const std::string& p) { + if (p.size() < 4) return false; + std::string ext = p.substr(p.size() - 4); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return ext == ".blp"; + }; + + std::vector capeCandidates; + auto addCapeCandidate = [&](const std::string& p) { + if (p.empty()) return; + if (std::find(capeCandidates.begin(), capeCandidates.end(), p) == capeCandidates.end()) { + capeCandidates.push_back(p); + } + }; + + for (const auto& nameRaw : capeNames) { + std::string name = nameRaw; + std::replace(name.begin(), name.end(), '/', '\\'); + bool hasDir = (name.find('\\') != std::string::npos); + bool hasExt = hasBlpExt(name); + if (hasDir) { + addCapeCandidate(name); + if (!hasExt) addCapeCandidate(name + ".blp"); + } else { + std::string base = "Item\\ObjectComponents\\Cape\\" + name; + addCapeCandidate(base); + if (!hasExt) addCapeCandidate(base + ".blp"); + // Some data sets use gender/unisex suffix variants. + addCapeCandidate(base + (npcIsFemale ? "_F.blp" : "_M.blp")); + addCapeCandidate(base + "_U.blp"); + } + } + + for (const auto& candidate : capeCandidates) { + if (assetManager->fileExists(candidate)) { + npcCapeTexturePath = candidate; + break; + } + } + } + } } // Use baked texture for body skin (types 1, 2) @@ -3642,8 +3739,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; // Humanoid NPCs typically use creature-skin texture types (11-13). - // Some models use 1/2 (character skin/object skin) depending on client/content. - if (texType == 1 || texType == 2 || texType == 11 || texType == 12 || texType == 13) { + // Keep type 2 (object skin) untouched so cloak/cape slots do not get face/body textures. + if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { charRenderer->setModelTexture(modelId, static_cast(ti), finalTex); LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath); hasHumanoidTexture = true; @@ -3718,7 +3815,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (npcSkinTex != 0 && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; - if (texType == 1 || texType == 2 || texType == 11 || texType == 12 || texType == 13) { + if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { charRenderer->setModelTexture(modelId, static_cast(ti), npcSkinTex); hasHumanoidTexture = true; } @@ -3765,6 +3862,28 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } } + + // Apply cape texture only to object-skin slots (type 2) so body/face + // textures never bleed onto cloaks. + if (!npcCapeTexturePath.empty() && modelData) { + GLuint capeTex = charRenderer->loadTexture(npcCapeTexturePath); + if (capeTex != 0) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 2) { + charRenderer->setModelTexture(modelId, static_cast(ti), capeTex); + LOG_DEBUG("Applied NPC cape texture to slot ", ti, ": ", npcCapeTexturePath); + } + } + } + } else if (modelData) { + // Hide cloak mesh when no cape texture exists for this NPC. + GLuint hiddenTex = charRenderer->getTransparentTexture(); + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + if (modelData->textures[ti].type == 2) { + charRenderer->setModelTexture(modelId, static_cast(ti), hiddenTex); + } + } + } } else { LOG_WARNING(" extraDisplayId ", dispData.extraDisplayId, " not found in humanoidExtraMap"); } @@ -3878,6 +3997,54 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x return; } + // Use a safe humanoid geoset mask to avoid rendering conflicting geosets + // (e.g. robe skirt + pants simultaneously) when model defaults expose all groups. + if (itDisplayData != displayDataMap_.end() && + itDisplayData->second.extraDisplayId != 0) { + auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId); + if (itExtra != humanoidExtraMap_.end()) { + const auto& extra = itExtra->second; + std::unordered_set safeGeosets; + for (uint16_t i = 0; i <= 99; i++) safeGeosets.insert(i); + + uint16_t hairGeoset = 1; + uint32_t hairKey = (static_cast(extra.raceId) << 16) | + (static_cast(extra.sexId) << 8) | + static_cast(extra.hairStyleId); + auto itHairGeo = hairGeosetMap_.find(hairKey); + if (itHairGeo != hairGeosetMap_.end() && itHairGeo->second > 0) { + hairGeoset = itHairGeo->second; + } + safeGeosets.insert(hairGeoset > 0 ? hairGeoset : 1); + safeGeosets.insert(static_cast(100 + std::max(hairGeoset, 1))); + + uint32_t facialKey = (static_cast(extra.raceId) << 16) | + (static_cast(extra.sexId) << 8) | + static_cast(extra.facialHairId); + auto itFacial = facialHairGeosetMap_.find(facialKey); + if (itFacial != facialHairGeosetMap_.end()) { + const auto& fhg = itFacial->second; + safeGeosets.insert(static_cast(200 + std::max(fhg.geoset200, 1))); + safeGeosets.insert(static_cast(300 + std::max(fhg.geoset300, 1))); + } else { + safeGeosets.insert(201); + safeGeosets.insert(301); + } + + // Force pants (1301) and avoid robe skirt variants unless we re-enable full slot-accurate geosets. + safeGeosets.insert(401); + safeGeosets.insert(502); + safeGeosets.insert(701); + safeGeosets.insert(801); + safeGeosets.insert(902); + safeGeosets.insert(1201); + safeGeosets.insert(1301); + safeGeosets.insert(1502); + safeGeosets.insert(2002); + charRenderer->setActiveGeosets(instanceId, safeGeosets); + } + } + // NOTE: Custom humanoid NPC geoset/equipment overrides are currently too // aggressive and can make NPCs invisible (targetable but not rendered). // Keep default model geosets for online creatures until this path is made @@ -4844,7 +5011,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (doodadTemplates && !doodadTemplates->empty()) { constexpr size_t kMaxTransportDoodads = 192; const size_t doodadBudget = std::min(doodadTemplates->size(), kMaxTransportDoodads); - LOG_INFO("Queueing ", doodadBudget, "/", doodadTemplates->size(), + LOG_DEBUG("Queueing ", doodadBudget, "/", doodadTemplates->size(), " transport doodads for WMO instance ", instanceId); pendingTransportDoodadBatches_.push_back(PendingTransportDoodadBatch{ guid, @@ -4857,8 +5024,8 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t orientation }); } else { - LOG_INFO("Transport WMO has no doodads or templates not available"); - } + LOG_DEBUG("Transport WMO has no doodads or templates not available"); + } } // Transport GameObjects are not always named "transport" in their WMO path @@ -5086,7 +5253,7 @@ void Application::processPendingTransportDoodads() { if (it->nextIndex >= maxIndex) { if (it->spawnedDoodads > 0) { - LOG_INFO("Spawned ", it->spawnedDoodads, + LOG_DEBUG("Spawned ", it->spawnedDoodads, " transport doodads for WMO instance ", it->instanceId); glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(it->x, it->y, it->z)); glm::mat4 wmoTransform(1.0f); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d38ea3f7..445542f8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7190,10 +7190,10 @@ void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) { uint32_t taxiPathId = data.data[0]; if (transportManager_->hasTaxiPath(taxiPathId)) { if (transportManager_->assignTaxiPathToTransport(data.entry, taxiPathId)) { - LOG_INFO("MO_TRANSPORT entry=", data.entry, " assigned TaxiPathNode path ", taxiPathId); + LOG_DEBUG("MO_TRANSPORT entry=", data.entry, " assigned TaxiPathNode path ", taxiPathId); } } else { - LOG_INFO("MO_TRANSPORT entry=", data.entry, " taxiPathId=", taxiPathId, + LOG_DEBUG("MO_TRANSPORT entry=", data.entry, " taxiPathId=", taxiPathId, " not found in TaxiPathNode.dbc"); } } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index d40ae9a2..9878a533 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -654,9 +654,9 @@ bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, } if (data.type == 15) { // MO_TRANSPORT - LOG_INFO("Classic GO query: MO_TRANSPORT entry=", data.entry, - " name=\"", data.name, "\" displayId=", data.displayId, - " taxiPathId=", data.data[0], " moveSpeed=", data.data[1]); + LOG_DEBUG("Classic GO query: MO_TRANSPORT entry=", data.entry, + " name=\"", data.name, "\" displayId=", data.displayId, + " taxiPathId=", data.data[0], " moveSpeed=", data.data[1]); } else { LOG_DEBUG("Classic GO query: ", data.name, " type=", data.type, " entry=", data.entry); } diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index a231352f..59c2cc00 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -215,9 +215,18 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { std::vector dbcData; + // Some visual DBC CSV exports are known to be malformed in community datasets + // (string columns shifted, missing numeric ID field). Force binary MPQ data for + // these tables to keep model/texture mappings correct. + const bool forceBinaryForVisualDbc = + (name == "CreatureDisplayInfo.dbc" || + name == "CreatureDisplayInfoExtra.dbc" || + name == "ItemDisplayInfo.dbc" || + name == "CreatureModelData.dbc"); + // Try expansion-specific CSV first (e.g. Data/expansions/wotlk/db/Spell.csv) bool loadedFromCSV = false; - if (!expansionDataPath_.empty()) { + if (!forceBinaryForVisualDbc && !expansionDataPath_.empty()) { // Derive CSV name from DBC name: "Spell.dbc" -> "Spell.csv" std::string baseName = name; auto dot = baseName.rfind('.'); @@ -239,6 +248,9 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { } } } + if (forceBinaryForVisualDbc && !expansionDataPath_.empty()) { + LOG_INFO("Skipping CSV override for visual DBC, using binary: ", name); + } // Fall back to manifest (binary DBC from extracted MPQs) if (dbcData.empty()) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index a89567d8..2f50323d 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -303,6 +303,14 @@ bool CharacterRenderer::initialize() { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Create 1x1 transparent fallback texture for hidden texture slots. + uint8_t transparent[] = { 0, 0, 0, 0 }; + glGenTextures(1, &transparentTexture); + glBindTexture(GL_TEXTURE_2D, transparentTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, transparent); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glBindTexture(GL_TEXTURE_2D, 0); // Diagnostics-only: cache lifetime is currently tied to renderer lifetime. @@ -345,6 +353,10 @@ void CharacterRenderer::shutdown() { glDeleteTextures(1, &whiteTexture); whiteTexture = 0; } + if (transparentTexture) { + glDeleteTextures(1, &transparentTexture); + transparentTexture = 0; + } models.clear(); instances.clear(); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 04e19ce2..9b01c724 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,16 @@ namespace rendering { namespace { +bool envFlagEnabled(const char* key, bool defaultValue) { + const char* raw = std::getenv(key); + if (!raw || !*raw) return defaultValue; + std::string v(raw); + std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return !(v == "0" || v == "false" || v == "off" || v == "no"); +} + static constexpr uint32_t kParticleFlagRandomized = 0x40; static constexpr uint32_t kParticleFlagTiled = 0x80; @@ -1248,19 +1259,21 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } - // Diagnostic: log batch details for light/lamp models to debug glow rendering - if (lowerName.find("light") != std::string::npos || - lowerName.find("lamp") != std::string::npos || - lowerName.find("lantern") != std::string::npos) { - LOG_INFO("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), - ": blend=", bgpu.blendMode, " matFlags=0x", - std::hex, bgpu.materialFlags, std::dec, - " colorKey=", bgpu.colorKeyBlack ? "Y" : "N", - " hasAlpha=", bgpu.hasAlpha ? "Y" : "N", - " unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N", - " glowSize=", bgpu.glowSize, - " tex=", bgpu.texture, - " idxCount=", bgpu.indexCount); + // Optional diagnostics for glow/light batches (disabled by default). + static const bool kGlowDiag = envFlagEnabled("WOWEE_M2_GLOW_DIAG", false); + if (kGlowDiag && + (lowerName.find("light") != std::string::npos || + lowerName.find("lamp") != std::string::npos || + lowerName.find("lantern") != std::string::npos)) { + LOG_DEBUG("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), + ": blend=", bgpu.blendMode, " matFlags=0x", + std::hex, bgpu.materialFlags, std::dec, + " colorKey=", bgpu.colorKeyBlack ? "Y" : "N", + " hasAlpha=", bgpu.hasAlpha ? "Y" : "N", + " unlit=", (bgpu.materialFlags & 0x01) ? "Y" : "N", + " glowSize=", bgpu.glowSize, + " tex=", bgpu.texture, + " idxCount=", bgpu.indexCount); } gpuModel.batches.push_back(bgpu); } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 85366f66..4c0b9317 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,16 @@ static std::unordered_map EMOTE_TABLE; static std::unordered_map EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo* static bool emoteTableLoaded = false; +static bool envFlagEnabled(const char* key, bool defaultValue) { + const char* raw = std::getenv(key); + if (!raw || !*raw) return defaultValue; + std::string v(raw); + std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return !(v == "0" || v == "false" || v == "off" || v == "no"); +} + static std::vector parseEmoteCommands(const std::string& raw) { std::vector out; std::string cur; @@ -250,6 +261,7 @@ Renderer::~Renderer() = default; bool Renderer::initialize(core::Window* win) { window = win; + deferredWorldInitEnabled_ = envFlagEnabled("WOWEE_DEFER_WORLD_SYSTEMS", true); LOG_INFO("Initializing renderer"); // Create camera (in front of Stormwind gate, looking north) @@ -1909,6 +1921,7 @@ void Renderer::update(float deltaTime) { if (musicSwitchCooldown_ > 0.0f) { musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime); } + runDeferredWorldInitStep(deltaTime); auto updateStart = std::chrono::steady_clock::now(); lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation() @@ -2455,6 +2468,46 @@ void Renderer::update(float deltaTime) { } } +void Renderer::runDeferredWorldInitStep(float deltaTime) { + if (!deferredWorldInitEnabled_ || !deferredWorldInitPending_ || !cachedAssetManager) return; + if (deferredWorldInitCooldown_ > 0.0f) { + deferredWorldInitCooldown_ = std::max(0.0f, deferredWorldInitCooldown_ - deltaTime); + if (deferredWorldInitCooldown_ > 0.0f) return; + } + + switch (deferredWorldInitStage_) { + case 0: + if (ambientSoundManager) { + ambientSoundManager->initialize(cachedAssetManager); + } + if (terrainManager && ambientSoundManager) { + terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + } + break; + case 1: + if (uiSoundManager) uiSoundManager->initialize(cachedAssetManager); + break; + case 2: + if (combatSoundManager) combatSoundManager->initialize(cachedAssetManager); + break; + case 3: + if (spellSoundManager) spellSoundManager->initialize(cachedAssetManager); + break; + case 4: + if (movementSoundManager) movementSoundManager->initialize(cachedAssetManager); + break; + case 5: + if (questMarkerRenderer) questMarkerRenderer->initialize(cachedAssetManager); + break; + default: + deferredWorldInitPending_ = false; + return; + } + + deferredWorldInitStage_++; + deferredWorldInitCooldown_ = 0.12f; +} + // ============================================================ // Selection Circle // ============================================================ @@ -3197,39 +3250,46 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (npcVoiceManager) { npcVoiceManager->initialize(assetManager); } - if (ambientSoundManager) { - ambientSoundManager->initialize(assetManager); - } - if (uiSoundManager) { - uiSoundManager->initialize(assetManager); - } - if (combatSoundManager) { - combatSoundManager->initialize(assetManager); - } - if (spellSoundManager) { - spellSoundManager->initialize(assetManager); - } - if (movementSoundManager) { - movementSoundManager->initialize(assetManager); - } - if (questMarkerRenderer) { - questMarkerRenderer->initialize(assetManager); - } - - // Prewarm frequently used zone/tavern music so zone transitions don't stall on MPQ I/O. - if (zoneManager) { - for (const auto& musicPath : zoneManager->getAllMusicPaths()) { - musicManager->preloadMusic(musicPath); + if (!deferredWorldInitEnabled_) { + if (ambientSoundManager) { + ambientSoundManager->initialize(assetManager); } - } - static const std::vector tavernTracks = { - "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3", - "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3", - "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3", - "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", - }; - for (const auto& musicPath : tavernTracks) { - musicManager->preloadMusic(musicPath); + if (uiSoundManager) { + uiSoundManager->initialize(assetManager); + } + if (combatSoundManager) { + combatSoundManager->initialize(assetManager); + } + if (spellSoundManager) { + spellSoundManager->initialize(assetManager); + } + if (movementSoundManager) { + movementSoundManager->initialize(assetManager); + } + if (questMarkerRenderer) { + questMarkerRenderer->initialize(assetManager); + } + + if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { + if (zoneManager) { + for (const auto& musicPath : zoneManager->getAllMusicPaths()) { + musicManager->preloadMusic(musicPath); + } + } + static const std::vector tavernTracks = { + "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance01.mp3", + "Sound\\Music\\ZoneMusic\\TavernAlliance\\TavernAlliance02.mp3", + "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern1A.mp3", + "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", + }; + for (const auto& musicPath : tavernTracks) { + musicManager->preloadMusic(musicPath); + } + } + } else { + deferredWorldInitPending_ = true; + deferredWorldInitStage_ = 0; + deferredWorldInitCooldown_ = 0.25f; } cachedAssetManager = assetManager; @@ -3316,23 +3376,29 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent if (npcVoiceManager && cachedAssetManager) { npcVoiceManager->initialize(cachedAssetManager); } - if (ambientSoundManager && cachedAssetManager) { - ambientSoundManager->initialize(cachedAssetManager); - } - if (uiSoundManager && cachedAssetManager) { - uiSoundManager->initialize(cachedAssetManager); - } - if (combatSoundManager && cachedAssetManager) { - combatSoundManager->initialize(cachedAssetManager); - } - if (spellSoundManager && cachedAssetManager) { - spellSoundManager->initialize(cachedAssetManager); - } - if (movementSoundManager && cachedAssetManager) { - movementSoundManager->initialize(cachedAssetManager); - } - if (questMarkerRenderer && cachedAssetManager) { - questMarkerRenderer->initialize(cachedAssetManager); + if (!deferredWorldInitEnabled_) { + if (ambientSoundManager && cachedAssetManager) { + ambientSoundManager->initialize(cachedAssetManager); + } + if (uiSoundManager && cachedAssetManager) { + uiSoundManager->initialize(cachedAssetManager); + } + if (combatSoundManager && cachedAssetManager) { + combatSoundManager->initialize(cachedAssetManager); + } + if (spellSoundManager && cachedAssetManager) { + spellSoundManager->initialize(cachedAssetManager); + } + if (movementSoundManager && cachedAssetManager) { + movementSoundManager->initialize(cachedAssetManager); + } + if (questMarkerRenderer && cachedAssetManager) { + questMarkerRenderer->initialize(cachedAssetManager); + } + } else { + deferredWorldInitPending_ = true; + deferredWorldInitStage_ = 0; + deferredWorldInitCooldown_ = 0.1f; } // Wire ambient sound manager to terrain manager for emitter registration