mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
Fix taxi state sync and transport authority; reduce runtime log overhead; restore first-person self-hide
This commit is contained in:
parent
40b50454ce
commit
5171f9cad4
29 changed files with 529 additions and 360 deletions
|
|
@ -898,7 +898,8 @@ void CameraController::update(float deltaTime) {
|
|||
// WoW fades between ~1.0m and ~0.5m, hides fully below 0.5m
|
||||
// For now, just hide below first-person threshold
|
||||
if (characterRenderer && playerInstanceId > 0) {
|
||||
bool shouldHidePlayer = (actualDist < MIN_DISTANCE + 0.1f); // Hide in first-person
|
||||
// Honor first-person intent even if anti-clipping pushes camera back slightly.
|
||||
bool shouldHidePlayer = isFirstPersonView() || (actualDist < MIN_DISTANCE + 0.1f);
|
||||
characterRenderer->setInstanceVisible(playerInstanceId, !shouldHidePlayer);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot,
|
|||
}
|
||||
|
||||
gpuModel.textureIds[textureSlot] = textureId;
|
||||
core::Logger::getInstance().info("Replaced model ", modelId, " texture slot ", textureSlot, " with composited texture");
|
||||
core::Logger::getInstance().debug("Replaced model ", modelId, " texture slot ", textureSlot, " with composited texture");
|
||||
}
|
||||
|
||||
void CharacterRenderer::resetModelTexture(uint32_t modelId, uint32_t textureSlot) {
|
||||
|
|
@ -773,9 +773,9 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) {
|
|||
|
||||
models[id] = std::move(gpuModel);
|
||||
|
||||
core::Logger::getInstance().info("Loaded M2 model ", id, " (", model.vertices.size(),
|
||||
" verts, ", model.bones.size(), " bones, ", model.sequences.size(),
|
||||
" anims, ", model.textures.size(), " textures)");
|
||||
core::Logger::getInstance().debug("Loaded M2 model ", id, " (", model.vertices.size(),
|
||||
" verts, ", model.bones.size(), " bones, ", model.sequences.size(),
|
||||
" anims, ", model.textures.size(), " textures)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1306,16 +1306,16 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
}
|
||||
|
||||
if (filtered) skipped++; else rendered++;
|
||||
LOG_INFO("Batch ", bIdx, ": submesh=", b.submeshId,
|
||||
" level=", b.submeshLevel,
|
||||
" idxStart=", b.indexStart, " idxCount=", b.indexCount,
|
||||
" tex=", texInfo,
|
||||
filtered ? " [SKIP]" : " [RENDER]");
|
||||
LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId,
|
||||
" level=", b.submeshLevel,
|
||||
" idxStart=", b.indexStart, " idxCount=", b.indexCount,
|
||||
" tex=", texInfo,
|
||||
filtered ? " [SKIP]" : " [RENDER]");
|
||||
bIdx++;
|
||||
}
|
||||
LOG_INFO("Batch summary: ", rendered, " rendered, ", skipped, " skipped, ",
|
||||
gpuModel.textureIds.size(), " textures loaded, ",
|
||||
gpuModel.data.textureLookup.size(), " in lookup table");
|
||||
LOG_DEBUG("Batch summary: ", rendered, " rendered, ", skipped, " skipped, ",
|
||||
gpuModel.textureIds.size(), " textures loaded, ",
|
||||
gpuModel.data.textureLookup.size(), " in lookup table");
|
||||
for (size_t t = 0; t < gpuModel.data.textures.size(); t++) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2520,6 +2520,24 @@ void M2Renderer::removeInstance(uint32_t instanceId) {
|
|||
}
|
||||
}
|
||||
|
||||
void M2Renderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
|
||||
if (instanceIds.empty() || instances.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
|
||||
const size_t oldSize = instances.size();
|
||||
instances.erase(std::remove_if(instances.begin(), instances.end(),
|
||||
[&toRemove](const M2Instance& inst) {
|
||||
return toRemove.find(inst.id) != toRemove.end();
|
||||
}),
|
||||
instances.end());
|
||||
|
||||
if (instances.size() != oldSize) {
|
||||
rebuildSpatialIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::clear() {
|
||||
for (auto& [id, model] : models) {
|
||||
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
|
||||
|
|
|
|||
|
|
@ -574,7 +574,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
}
|
||||
|
||||
// Discover mount animation capabilities (property-based, not hardcoded IDs)
|
||||
LOG_INFO("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
|
||||
LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ===");
|
||||
characterRenderer->dumpAnimations(mountInstId);
|
||||
|
||||
// Get all sequences for property-based analysis
|
||||
|
|
@ -597,9 +597,9 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
// Property-based jump animation discovery with chain-based scoring
|
||||
auto discoverJumpSet = [&]() {
|
||||
// Debug: log all sequences for analysis
|
||||
LOG_INFO("=== Full sequence table for mount ===");
|
||||
LOG_DEBUG("=== Full sequence table for mount ===");
|
||||
for (const auto& seq : sequences) {
|
||||
LOG_INFO("SEQ id=", seq.id,
|
||||
LOG_DEBUG("SEQ id=", seq.id,
|
||||
" dur=", seq.duration,
|
||||
" flags=0x", std::hex, seq.flags, std::dec,
|
||||
" moveSpd=", seq.movingSpeed,
|
||||
|
|
@ -607,7 +607,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
" next=", seq.nextAnimation,
|
||||
" alias=", seq.aliasNext);
|
||||
}
|
||||
LOG_INFO("=== End sequence table ===");
|
||||
LOG_DEBUG("=== End sequence table ===");
|
||||
|
||||
// Known combat/bad animation IDs to avoid
|
||||
std::set<uint32_t> forbiddenIds = {53, 54, 16}; // jumpkick, attack
|
||||
|
|
@ -701,7 +701,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
|
||||
LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end,
|
||||
" scores: start=", bestStart, " end=", bestEnd);
|
||||
return std::make_tuple(start, loop, end);
|
||||
};
|
||||
|
|
@ -718,17 +718,17 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
|
||||
// Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers)
|
||||
mountAnims_.fidgets.clear();
|
||||
core::Logger::getInstance().info("Scanning for fidget animations in ", sequences.size(), " sequences");
|
||||
core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences");
|
||||
|
||||
// DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses
|
||||
core::Logger::getInstance().info("=== ALL potential fidgets (no metadata filter) ===");
|
||||
core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ===");
|
||||
for (const auto& seq : sequences) {
|
||||
bool isLoop = (seq.flags & 0x01) == 0;
|
||||
bool isStationary = std::abs(seq.movingSpeed) < 0.05f;
|
||||
bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500;
|
||||
|
||||
if (!isLoop && reasonableDuration && isStationary) {
|
||||
core::Logger::getInstance().info(" ALL: id=", seq.id,
|
||||
core::Logger::getInstance().debug(" ALL: id=", seq.id,
|
||||
" dur=", seq.duration, "ms",
|
||||
" freq=", seq.frequency,
|
||||
" replay=", seq.replayMin, "-", seq.replayMax,
|
||||
|
|
@ -747,7 +747,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
|
||||
// Log candidates with metadata
|
||||
if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) {
|
||||
core::Logger::getInstance().info(" Candidate: id=", seq.id,
|
||||
core::Logger::getInstance().debug(" Candidate: id=", seq.id,
|
||||
" dur=", seq.duration, "ms",
|
||||
" freq=", seq.frequency,
|
||||
" replay=", seq.replayMin, "-", seq.replayMax,
|
||||
|
|
@ -770,7 +770,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
(seq.nextAnimation == -1);
|
||||
|
||||
mountAnims_.fidgets.push_back(seq.id);
|
||||
core::Logger::getInstance().info(" >> Selected fidget: id=", seq.id,
|
||||
core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id,
|
||||
(chainsToStand ? " (chains to stand)" : ""));
|
||||
}
|
||||
}
|
||||
|
|
@ -779,7 +779,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
if (mountAnims_.stand == 0) mountAnims_.stand = 0; // Force 0 even if not found
|
||||
if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run
|
||||
|
||||
core::Logger::getInstance().info("Mount animation set: jumpStart=", mountAnims_.jumpStart,
|
||||
core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart,
|
||||
" jumpLoop=", mountAnims_.jumpLoop,
|
||||
" jumpEnd=", mountAnims_.jumpEnd,
|
||||
" rearUp=", mountAnims_.rearUp,
|
||||
|
|
@ -1001,7 +1001,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) {
|
||||
if (moving && mountAnims_.jumpLoop > 0) {
|
||||
// Moving: skip JumpStart (looks like stopping), go straight to airborne loop
|
||||
LOG_INFO("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop);
|
||||
LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop);
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
|
||||
mountAction_ = MountAction::Jump;
|
||||
mountActionPhase_ = 1; // Start in airborne phase
|
||||
|
|
@ -1014,7 +1014,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
}
|
||||
} else if (!moving && mountAnims_.rearUp > 0) {
|
||||
// Standing still: rear-up flourish
|
||||
LOG_INFO("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp);
|
||||
LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp);
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false);
|
||||
mountAction_ = MountAction::RearUp;
|
||||
mountActionPhase_ = 0;
|
||||
|
|
@ -1035,17 +1035,17 @@ void Renderer::updateCharacterAnimation() {
|
|||
// Jump sequence: start → loop → end (physics-driven)
|
||||
if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) {
|
||||
// JumpStart finished, go to JumpLoop (airborne)
|
||||
LOG_INFO("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")");
|
||||
LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")");
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true);
|
||||
mountActionPhase_ = 1;
|
||||
mountAnimId = mountAnims_.jumpLoop;
|
||||
} else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) {
|
||||
// No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose)
|
||||
LOG_INFO("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)");
|
||||
LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)");
|
||||
mountActionPhase_ = 1;
|
||||
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) {
|
||||
// Landed after airborne phase! Go to JumpEnd (grounded-triggered)
|
||||
LOG_INFO("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")");
|
||||
LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")");
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false);
|
||||
mountActionPhase_ = 2;
|
||||
mountAnimId = mountAnims_.jumpEnd;
|
||||
|
|
@ -1055,14 +1055,14 @@ void Renderer::updateCharacterAnimation() {
|
|||
}
|
||||
} else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) {
|
||||
// No JumpEnd animation, return directly to movement after landing
|
||||
LOG_INFO("Mount jump: phase 1→done (landed, no JumpEnd, returning to ",
|
||||
LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ",
|
||||
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
|
||||
mountAction_ = MountAction::None;
|
||||
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
||||
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
||||
} else if (mountActionPhase_ == 2 && animFinished) {
|
||||
// JumpEnd finished, return to movement
|
||||
LOG_INFO("Mount jump: phase 2→done (JumpEnd finished, returning to ",
|
||||
LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ",
|
||||
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")");
|
||||
mountAction_ = MountAction::None;
|
||||
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
||||
|
|
@ -1073,7 +1073,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
} else if (mountAction_ == MountAction::RearUp) {
|
||||
// Rear-up: single animation, return to stand when done
|
||||
if (animFinished) {
|
||||
LOG_INFO("Mount rear-up: finished, returning to ",
|
||||
LOG_DEBUG("Mount rear-up: finished, returning to ",
|
||||
moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand));
|
||||
mountAction_ = MountAction::None;
|
||||
mountAnimId = moving ? mountAnims_.run : mountAnims_.stand;
|
||||
|
|
@ -1110,7 +1110,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
// If animation changed or completed, clear active fidget
|
||||
if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) {
|
||||
mountActiveFidget_ = 0;
|
||||
LOG_INFO("Mount fidget completed");
|
||||
LOG_DEBUG("Mount fidget completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1131,7 +1131,7 @@ void Renderer::updateCharacterAnimation() {
|
|||
mountIdleFidgetTimer_ = 0.0f;
|
||||
nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time
|
||||
|
||||
LOG_INFO("Mount idle fidget: playing anim ", fidgetAnim);
|
||||
LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim);
|
||||
}
|
||||
}
|
||||
if (moving) {
|
||||
|
|
@ -1666,6 +1666,10 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
|||
}
|
||||
|
||||
void Renderer::update(float deltaTime) {
|
||||
if (musicSwitchCooldown_ > 0.0f) {
|
||||
musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime);
|
||||
}
|
||||
|
||||
auto updateStart = std::chrono::steady_clock::now();
|
||||
lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation()
|
||||
|
||||
|
|
@ -1696,6 +1700,14 @@ void Renderer::update(float deltaTime) {
|
|||
auto cam2 = std::chrono::high_resolution_clock::now();
|
||||
camTime += std::chrono::duration<float, std::milli>(cam2 - cam1).count();
|
||||
|
||||
// Visibility hardening: ensure player instance cannot stay hidden after
|
||||
// taxi/camera transitions, but preserve first-person self-hide.
|
||||
if (characterRenderer && characterInstanceId > 0 && cameraController) {
|
||||
if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || taxiFlight_) {
|
||||
characterRenderer->setInstanceVisible(characterInstanceId, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Update lighting system
|
||||
auto light1 = std::chrono::high_resolution_clock::now();
|
||||
if (lightingManager) {
|
||||
|
|
@ -2082,6 +2094,7 @@ void Renderer::update(float deltaTime) {
|
|||
inTavern_ = true;
|
||||
LOG_INFO("Entered tavern");
|
||||
musicManager->playMusic(tavernMusic, true); // Immediate playback, looping
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
} else if (inTavern_) {
|
||||
// Exited tavern - restore zone music with crossfade
|
||||
|
|
@ -2092,6 +2105,7 @@ void Renderer::update(float deltaTime) {
|
|||
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
||||
if (!music.empty()) {
|
||||
musicManager->crossfadeTo(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2112,6 +2126,7 @@ void Renderer::update(float deltaTime) {
|
|||
std::string music = zoneManager->getRandomMusic(currentZoneId);
|
||||
if (!music.empty()) {
|
||||
musicManager->crossfadeTo(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2123,9 +2138,12 @@ void Renderer::update(float deltaTime) {
|
|||
if (info) {
|
||||
currentZoneName = info->name;
|
||||
LOG_INFO("Entered zone: ", info->name);
|
||||
std::string music = zoneManager->getRandomMusic(zoneId);
|
||||
if (!music.empty()) {
|
||||
musicManager->crossfadeTo(music);
|
||||
if (musicSwitchCooldown_ <= 0.0f) {
|
||||
std::string music = zoneManager->getRandomMusic(zoneId);
|
||||
if (!music.empty()) {
|
||||
musicManager->crossfadeTo(music);
|
||||
musicSwitchCooldown_ = 6.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
auto adtData = assetManager->readFile(adtPath);
|
||||
|
||||
if (adtData.empty()) {
|
||||
LOG_WARNING("Failed to load ADT file: ", adtPath);
|
||||
logMissingAdtOnce(adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +322,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
preparedModelIds.insert(modelId);
|
||||
} else {
|
||||
skippedInvalid++;
|
||||
LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path);
|
||||
LOG_DEBUG("M2 model invalid (no verts/indices): ", m2Path);
|
||||
}
|
||||
} else {
|
||||
skippedFileNotFound++;
|
||||
|
|
@ -352,7 +352,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
}
|
||||
|
||||
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) {
|
||||
LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ",
|
||||
LOG_DEBUG("Tile [", x, ",", y, "] doodad issues: ",
|
||||
skippedNameId, " bad nameId, ",
|
||||
skippedFileNotFound, " file not found, ",
|
||||
skippedInvalid, " invalid model, ",
|
||||
|
|
@ -547,6 +547,17 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
return pending;
|
||||
}
|
||||
|
||||
void TerrainManager::logMissingAdtOnce(const std::string& adtPath) {
|
||||
std::string normalized = adtPath;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
std::lock_guard<std::mutex> lock(missingAdtWarningsMutex_);
|
||||
if (missingAdtWarnings_.insert(normalized).second) {
|
||||
LOG_WARNING("Failed to load ADT file: ", adtPath);
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
|
||||
int x = pending->coord.x;
|
||||
int y = pending->coord.y;
|
||||
|
|
@ -723,7 +734,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
|
|||
}
|
||||
}
|
||||
if (loadedWMOs > 0 || skippedWmoDedup > 0) {
|
||||
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ",
|
||||
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ",
|
||||
loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped");
|
||||
}
|
||||
if (loadedLiquids > 0) {
|
||||
|
|
@ -817,8 +828,8 @@ void TerrainManager::workerLoop() {
|
|||
|
||||
void TerrainManager::processReadyTiles() {
|
||||
// Process tiles with time budget to avoid frame spikes
|
||||
// Budget: 5ms per frame (allows 3 tiles at ~1.5ms each or 1 heavy tile)
|
||||
const float timeBudgetMs = 5.0f;
|
||||
// Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models.
|
||||
const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 5.0f;
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
int processed = 0;
|
||||
|
||||
|
|
@ -1010,9 +1021,7 @@ void TerrainManager::unloadTile(int x, int y) {
|
|||
|
||||
// Remove M2 doodad instances
|
||||
if (m2Renderer) {
|
||||
for (uint32_t id : tile->m2InstanceIds) {
|
||||
m2Renderer->removeInstance(id);
|
||||
}
|
||||
m2Renderer->removeInstances(tile->m2InstanceIds);
|
||||
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
|
||||
}
|
||||
|
||||
|
|
@ -1023,8 +1032,8 @@ void TerrainManager::unloadTile(int x, int y) {
|
|||
if (waterRenderer) {
|
||||
waterRenderer->removeWMO(id);
|
||||
}
|
||||
wmoRenderer->removeInstance(id);
|
||||
}
|
||||
wmoRenderer->removeInstances(tile->wmoInstanceIds);
|
||||
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
|
||||
}
|
||||
|
||||
|
|
@ -1328,6 +1337,18 @@ std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float
|
|||
}
|
||||
|
||||
void TerrainManager::streamTiles() {
|
||||
auto shouldSkipMissingAdt = [this](const TileCoord& coord) -> bool {
|
||||
if (!assetManager) return false;
|
||||
if (failedTiles.find(coord) != failedTiles.end()) return true;
|
||||
const std::string adtPath = getADTPath(coord);
|
||||
if (!assetManager->fileExists(adtPath)) {
|
||||
// Mark permanently failed so future stream/precache passes do not retry.
|
||||
failedTiles[coord] = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Enqueue tiles in radius around current tile for async loading
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
|
|
@ -1353,6 +1374,7 @@ void TerrainManager::streamTiles() {
|
|||
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
|
||||
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
|
||||
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
||||
if (shouldSkipMissingAdt(coord)) continue;
|
||||
|
||||
loadQueue.push_back(coord);
|
||||
pendingTiles[coord] = true;
|
||||
|
|
@ -1403,12 +1425,18 @@ void TerrainManager::precacheTiles(const std::vector<std::pair<int, int>>& tiles
|
|||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
|
||||
for (const auto& [x, y] : tiles) {
|
||||
if (x < 0 || x > 63 || y < 0 || y > 63) continue;
|
||||
|
||||
TileCoord coord = {x, y};
|
||||
|
||||
// Skip if already loaded, pending, or failed
|
||||
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
|
||||
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
|
||||
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
||||
if (assetManager && !assetManager->fileExists(getADTPath(coord))) {
|
||||
failedTiles[coord] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Precache work is prioritized so taxi-route tiles are prepared before
|
||||
// opportunistic radius streaming tiles.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,17 @@ bool TerrainRenderer::initialize(pipeline::AssetManager* assets) {
|
|||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// Create default opaque alpha texture for terrain layer masks
|
||||
uint8_t opaqueAlpha = 255;
|
||||
glGenTextures(1, &opaqueAlphaTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, opaqueAlphaTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &opaqueAlpha);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
LOG_INFO("Terrain renderer initialized");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -60,6 +71,10 @@ void TerrainRenderer::shutdown() {
|
|||
glDeleteTextures(1, &whiteTexture);
|
||||
whiteTexture = 0;
|
||||
}
|
||||
if (opaqueAlphaTexture) {
|
||||
glDeleteTextures(1, &opaqueAlphaTexture);
|
||||
opaqueAlphaTexture = 0;
|
||||
}
|
||||
|
||||
// Delete cached textures
|
||||
for (auto& pair : textureCache) {
|
||||
|
|
@ -73,7 +88,7 @@ void TerrainRenderer::shutdown() {
|
|||
bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
|
||||
const std::vector<std::string>& texturePaths,
|
||||
int tileX, int tileY) {
|
||||
LOG_INFO("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
|
||||
LOG_DEBUG("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
|
||||
|
||||
// Upload each chunk to GPU
|
||||
for (int y = 0; y < 16; y++) {
|
||||
|
|
@ -116,7 +131,7 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
|
|||
gpuChunk.layerTextures.push_back(layerTex);
|
||||
|
||||
// Create alpha texture
|
||||
GLuint alphaTex = 0;
|
||||
GLuint alphaTex = opaqueAlphaTexture;
|
||||
if (!layer.alphaData.empty()) {
|
||||
alphaTex = createAlphaTexture(layer.alphaData);
|
||||
}
|
||||
|
|
@ -133,7 +148,7 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", chunks.size(), " terrain chunks to GPU");
|
||||
LOG_DEBUG("Loaded ", chunks.size(), " terrain chunks to GPU");
|
||||
return !chunks.empty();
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +233,8 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) {
|
|||
pipeline::BLPImage blp = assetManager->loadTexture(path);
|
||||
if (!blp.isValid()) {
|
||||
LOG_WARNING("Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
// Do not cache failure as white: MPQ/file reads can fail transiently
|
||||
// during heavy streaming and should be allowed to recover.
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +273,8 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map<std::stri
|
|||
// Skip if already cached
|
||||
if (textureCache.find(path) != textureCache.end()) continue;
|
||||
if (!blp.isValid()) {
|
||||
textureCache[path] = whiteTexture;
|
||||
// Don't poison cache with white on invalid preload; allow fallback
|
||||
// path to retry loading this texture later.
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -281,20 +298,32 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map<std::stri
|
|||
|
||||
GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) {
|
||||
if (alphaData.empty()) {
|
||||
return 0;
|
||||
return opaqueAlphaTexture;
|
||||
}
|
||||
|
||||
if (alphaData.size() != 4096) {
|
||||
LOG_WARNING("Unexpected terrain alpha size: ", alphaData.size(), " (expected 4096)");
|
||||
}
|
||||
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
// Alpha data is always expanded to 4096 bytes (64x64 at 8-bit) by terrain_mesh
|
||||
// Alpha data should be 64x64 (4096 bytes). Clamp to a sane fallback when malformed.
|
||||
std::vector<uint8_t> expanded;
|
||||
const uint8_t* src = alphaData.data();
|
||||
if (alphaData.size() < 4096) {
|
||||
expanded.assign(4096, 255);
|
||||
std::copy(alphaData.begin(), alphaData.end(), expanded.begin());
|
||||
src = expanded.data();
|
||||
}
|
||||
|
||||
int width = 64;
|
||||
int height = static_cast<int>(alphaData.size()) / 64;
|
||||
int height = 64;
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,
|
||||
width, height, 0,
|
||||
GL_RED, GL_UNSIGNED_BYTE, alphaData.data());
|
||||
GL_RED, GL_UNSIGNED_BYTE, src);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
|
@ -329,6 +358,9 @@ void TerrainRenderer::render(const Camera& camera) {
|
|||
// Enable depth testing
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
// Disable backface culling temporarily to debug flashing
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
|
|
|||
|
|
@ -184,10 +184,10 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
constexpr float TILE_SIZE = 33.33333f / 8.0f;
|
||||
|
||||
if (!append) {
|
||||
LOG_INFO("Loading water from terrain (replacing)");
|
||||
LOG_DEBUG("Loading water from terrain (replacing)");
|
||||
clear();
|
||||
} else {
|
||||
LOG_INFO("Loading water from terrain (appending)");
|
||||
LOG_DEBUG("Loading water from terrain (appending)");
|
||||
}
|
||||
|
||||
// Load water surfaces from MH2O data
|
||||
|
|
@ -285,14 +285,14 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
glm::vec2(moonwellPos.x, moonwellPos.y));
|
||||
|
||||
if (distToMoonwell > 300.0f) { // Terrain tiles are large, use bigger exclusion radius
|
||||
LOG_INFO(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit");
|
||||
LOG_DEBUG(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit");
|
||||
for (float& h : surface.heights) {
|
||||
h -= 1.0f;
|
||||
}
|
||||
surface.minHeight -= 1.0f;
|
||||
surface.maxHeight -= 1.0f;
|
||||
} else {
|
||||
LOG_INFO(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")");
|
||||
LOG_DEBUG(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,7 +307,7 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data");
|
||||
LOG_DEBUG("Loaded ", totalLayers, " water layers from MH2O data");
|
||||
}
|
||||
|
||||
void WaterRenderer::removeTile(int tileX, int tileY) {
|
||||
|
|
@ -391,7 +391,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
|||
int tileY = static_cast<int>(std::floor((32.0f - surface.origin.y / 533.33333f)));
|
||||
|
||||
// Log all WMO water to debug park issue
|
||||
LOG_INFO("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
|
||||
LOG_DEBUG("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
|
||||
") tile=(", tileX, ",", tileY, ") wmoId=", wmoId);
|
||||
|
||||
// Expanded bounds to cover all of Stormwind including outlying areas and park
|
||||
|
|
@ -405,27 +405,27 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
|||
glm::vec2(moonwellPos.x, moonwellPos.y));
|
||||
|
||||
if (distToMoonwell > 20.0f) {
|
||||
LOG_INFO(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")");
|
||||
LOG_DEBUG(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")");
|
||||
for (float& h : surface.heights) {
|
||||
h -= 1.0f;
|
||||
}
|
||||
surface.minHeight -= 1.0f;
|
||||
surface.maxHeight -= 1.0f;
|
||||
} else {
|
||||
LOG_INFO(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")");
|
||||
LOG_DEBUG(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")");
|
||||
}
|
||||
}
|
||||
|
||||
// Skip WMO water that's clearly invalid (extremely high - above 300 units)
|
||||
// This is a conservative global filter that won't affect normal gameplay
|
||||
if (surface.origin.z > 300.0f) {
|
||||
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)");
|
||||
LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip WMO water that's extremely low (deep underground where it shouldn't be)
|
||||
if (surface.origin.z < -100.0f) {
|
||||
LOG_INFO("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)");
|
||||
LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -268,8 +268,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
return true;
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
||||
model.textures.size(), " textures...");
|
||||
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
||||
model.textures.size(), " textures...");
|
||||
|
||||
ModelData modelData;
|
||||
modelData.id = id;
|
||||
|
|
@ -282,11 +282,11 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
modelData.isLowPlatform = (vert < 6.0f && horiz > 20.0f);
|
||||
}
|
||||
|
||||
core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
|
||||
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
|
||||
core::Logger::getInstance().debug(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
|
||||
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
|
||||
|
||||
// Load textures for this model
|
||||
core::Logger::getInstance().info(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
|
||||
core::Logger::getInstance().debug(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
|
||||
if (assetManager && !model.textures.empty()) {
|
||||
for (size_t i = 0; i < model.textures.size(); i++) {
|
||||
const auto& texPath = model.textures[i];
|
||||
|
|
@ -294,13 +294,13 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
GLuint texId = loadTexture(texPath);
|
||||
modelData.textures.push_back(texId);
|
||||
}
|
||||
core::Logger::getInstance().info(" Loaded ", modelData.textures.size(), " textures for WMO");
|
||||
core::Logger::getInstance().debug(" Loaded ", modelData.textures.size(), " textures for WMO");
|
||||
}
|
||||
|
||||
// Store material -> texture index mapping
|
||||
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
|
||||
// We need to convert it using the textureOffsetToIndex map
|
||||
core::Logger::getInstance().info(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
|
||||
core::Logger::getInstance().debug(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
|
||||
static int matLogCount = 0;
|
||||
for (size_t i = 0; i < model.materials.size(); i++) {
|
||||
const auto& mat = model.materials[i];
|
||||
|
|
@ -310,19 +310,19 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
if (it != model.textureOffsetToIndex.end()) {
|
||||
texIndex = it->second;
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
|
||||
core::Logger::getInstance().debug(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
|
||||
matLogCount++;
|
||||
}
|
||||
} else if (mat.texture1 < model.textures.size()) {
|
||||
// Fallback: maybe it IS an index in some files?
|
||||
texIndex = mat.texture1;
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": using texture1 as direct index: ", texIndex);
|
||||
core::Logger::getInstance().debug(" Material ", i, ": using texture1 as direct index: ", texIndex);
|
||||
matLogCount++;
|
||||
}
|
||||
} else {
|
||||
if (matLogCount < 20) {
|
||||
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
|
||||
core::Logger::getInstance().debug(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
|
||||
matLogCount++;
|
||||
}
|
||||
}
|
||||
|
|
@ -435,8 +435,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
}
|
||||
|
||||
if (!modelData.portals.empty()) {
|
||||
core::Logger::getInstance().info("WMO portals: ", modelData.portals.size(),
|
||||
" refs: ", modelData.portalRefs.size());
|
||||
core::Logger::getInstance().debug("WMO portals: ", modelData.portals.size(),
|
||||
" refs: ", modelData.portalRefs.size());
|
||||
}
|
||||
|
||||
// Store doodad templates (M2 models placed in WMO) for instancing later
|
||||
|
|
@ -478,12 +478,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
}
|
||||
|
||||
if (!modelData.doodadTemplates.empty()) {
|
||||
core::Logger::getInstance().info("WMO has ", modelData.doodadTemplates.size(), " doodad templates");
|
||||
core::Logger::getInstance().debug("WMO has ", modelData.doodadTemplates.size(), " doodad templates");
|
||||
}
|
||||
}
|
||||
|
||||
loadedModels[id] = std::move(modelData);
|
||||
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||
core::Logger::getInstance().debug("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -570,7 +570,7 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position
|
|||
}
|
||||
}
|
||||
}
|
||||
core::Logger::getInstance().info("Created WMO instance ", instance.id, " (model ", modelId, ")");
|
||||
core::Logger::getInstance().debug("Created WMO instance ", instance.id, " (model ", modelId, ")");
|
||||
return instance.id;
|
||||
}
|
||||
|
||||
|
|
@ -677,7 +677,27 @@ void WMORenderer::removeInstance(uint32_t instanceId) {
|
|||
if (it != instances.end()) {
|
||||
instances.erase(it);
|
||||
rebuildSpatialIndex();
|
||||
core::Logger::getInstance().info("Removed WMO instance ", instanceId);
|
||||
core::Logger::getInstance().debug("Removed WMO instance ", instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
void WMORenderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
|
||||
if (instanceIds.empty() || instances.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
|
||||
const size_t oldSize = instances.size();
|
||||
instances.erase(std::remove_if(instances.begin(), instances.end(),
|
||||
[&toRemove](const WMOInstance& inst) {
|
||||
return toRemove.find(inst.id) != toRemove.end();
|
||||
}),
|
||||
instances.end());
|
||||
|
||||
if (instances.size() != oldSize) {
|
||||
rebuildSpatialIndex();
|
||||
core::Logger::getInstance().debug("Removed ", (oldSize - instances.size()),
|
||||
" WMO instances (batched)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1582,7 +1602,9 @@ GLuint WMORenderer::loadTexture(const std::string& path) {
|
|||
pipeline::BLPImage blp = assetManager->loadTexture(path);
|
||||
if (!blp.isValid()) {
|
||||
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
||||
textureCache[path] = whiteTexture;
|
||||
// Do not cache failures as white. MPQ reads can fail transiently
|
||||
// during streaming/contention, and caching white here permanently
|
||||
// poisons the texture for this session.
|
||||
return whiteTexture;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue