Fix NPC apparel fallback and reduce world-entry stutter

Hide NPC cloak/object-skin mesh when no cape texture resolves by using a transparent texture fallback, preventing skin-texture bleed on cloaks. Tighten NPC equipment region compositing by slot and add safe humanoid geoset selection to avoid robe-over-pants conflicts and odd pants texturing.

Reduce login/runtime hitching by deferring non-critical world-system initialization across frames, lowering per-frame transport doodad spawn budget, and demoting high-volume transport/MO_TRANSPORT diagnostics to debug. Gate M2 glow diagnostics behind WOWEE_M2_GLOW_DIAG and make zone music prewarm opt-in via WOWEE_PREWARM_ZONE_MUSIC.
This commit is contained in:
Kelsi 2026-02-20 20:31:04 -08:00
parent 48d9de810d
commit 3368dbb9ec
10 changed files with 369 additions and 91 deletions

View file

@ -303,7 +303,7 @@ private:
float orientation = 0.0f;
};
std::vector<PendingTransportDoodadBatch> 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)

View file

@ -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<uint32_t, M2ModelGPU> models;
std::unordered_map<uint32_t, CharacterInstance> instances;

View file

@ -176,6 +176,8 @@ public:
LightingManager* getLightingManager() { return lightingManager.get(); }
private:
void runDeferredWorldInitStep(float deltaTime);
core::Window* window = nullptr;
std::unique_ptr<Camera> camera;
std::unique_ptr<CameraController> 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);

View file

@ -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<glm::vec3> 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<std::pair<int, std::string>> 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<uint32_t>(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<std::string> 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<uint32_t>(capeRecIdx), leftTexField);
std::string rightName = npcItemDisplayDbc->getString(
static_cast<uint32_t>(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<char>(std::tolower(c)); });
return ext == ".blp";
};
std::vector<std::string> 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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint16_t> safeGeosets;
for (uint16_t i = 0; i <= 99; i++) safeGeosets.insert(i);
uint16_t hairGeoset = 1;
uint32_t hairKey = (static_cast<uint32_t>(extra.raceId) << 16) |
(static_cast<uint32_t>(extra.sexId) << 8) |
static_cast<uint32_t>(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<uint16_t>(100 + std::max<uint16_t>(hairGeoset, 1)));
uint32_t facialKey = (static_cast<uint32_t>(extra.raceId) << 16) |
(static_cast<uint32_t>(extra.sexId) << 8) |
static_cast<uint32_t>(extra.facialHairId);
auto itFacial = facialHairGeosetMap_.find(facialKey);
if (itFacial != facialHairGeosetMap_.end()) {
const auto& fhg = itFacial->second;
safeGeosets.insert(static_cast<uint16_t>(200 + std::max<uint16_t>(fhg.geoset200, 1)));
safeGeosets.insert(static_cast<uint16_t>(300 + std::max<uint16_t>(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);

View file

@ -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");
}
}

View file

@ -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);
}

View file

@ -215,9 +215,18 @@ std::shared_ptr<DBCFile> AssetManager::loadDBC(const std::string& name) {
std::vector<uint8_t> 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<DBCFile> 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()) {

View file

@ -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();

View file

@ -15,6 +15,7 @@
#include <functional>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <future>
#include <thread>
@ -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<char>(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);
}

View file

@ -57,6 +57,7 @@
#include <cctype>
#include <cmath>
#include <chrono>
#include <cstdlib>
#include <optional>
#include <unordered_map>
#include <unordered_set>
@ -80,6 +81,16 @@ static std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE;
static std::unordered_map<uint32_t, const EmoteInfo*> 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<char>(std::tolower(c));
});
return !(v == "0" || v == "false" || v == "off" || v == "no");
}
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
std::vector<std::string> 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<std::string> 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<std::string> 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