mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
48d9de810d
commit
3368dbb9ec
10 changed files with 369 additions and 91 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue