fix(render): code quality cleanup

Magic number elimination:
- Create protocol_constants.hpp, warden_constants.hpp,
  render_constants.hpp, ui_constants.hpp
- Replace ~55 magic numbers across game_handler, warden_handler,
  m2_renderer_render

Reduce nesting depth:
- Extract 5 parseEffect* methods from handleSpellLogExecute
  (max indent 52 → 16 cols)
- Extract resolveSpellSchool/playSpellCastSound/playSpellImpactSound
  from 3× duplicate audio blocks in handleSpellGo
- Flatten SMSG_INVENTORY_CHANGE_FAILURE with early-return guards
- Extract drawScreenEdgeVignette() for 3 duplicate vignette blocks

DRY extract patterns:
- Replace 12 compound expansion checks with isPreWotlk() across
  movement_handler (9), chat_handler (1), social_handler (1)

const to constexpr:
- Promote 23+ static const arrays/scalars to static constexpr across
  12 source files

Error handling:
- Convert PIN auth from exceptions to std::optional<PinProof>
- Add [[nodiscard]] to 15+ initialize/parse methods
- Wrap ~20 unchecked initialize() calls with LOG_WARNING/LOG_ERROR

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-06 22:43:13 +03:00
parent 2e8856bacd
commit 97106bd6ae
41 changed files with 849 additions and 424 deletions

View file

@ -2130,7 +2130,9 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
}
const float renderRadius = static_cast<float>(envSizeOrDefault("WOWEE_CHAR_RENDER_RADIUS", 130));
const float renderRadiusSq = renderRadius * renderRadius;
const float characterCullRadius = 2.0f; // Estimate character radius for frustum testing
// Default frustum-cull radius when model bounds aren't available.
// 4.0 covers Tauren, mounted characters, and most creature models.
constexpr float kDefaultCharacterCullRadius = 4.0f;
const glm::vec3 camPos = camera.getPosition();
// Extract frustum planes for per-instance visibility testing
@ -2175,8 +2177,17 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
// Distance cull: skip if beyond render radius
if (distSq > renderRadiusSq) continue;
// Compute per-instance bounding radius from model data when available.
float cullRadius = kDefaultCharacterCullRadius;
auto mIt = models.find(instance.modelId);
if (mIt != models.end()) {
float modelR = mIt->second.data.boundRadius;
if (modelR > 0.01f)
cullRadius = std::max(kDefaultCharacterCullRadius, modelR * std::max(0.001f, instance.scale));
}
// Frustum cull: skip if outside view frustum
if (!frustum.intersectsSphere(instance.position, characterCullRadius)) continue;
if (!frustum.intersectsSphere(instance.position, cullRadius)) continue;
}
if (!instance.cachedModel) continue;

View file

@ -11,6 +11,7 @@
#include "rendering/vk_frame_data.hpp"
#include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
#include "rendering/render_constants.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
@ -90,7 +91,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
instance.idleSequenceIndex = 0;
instance.animDuration = static_cast<float>(mdl.sequences[0].duration);
instance.animTime = static_cast<float>(randRange(std::max(1u, mdl.sequences[0].duration)));
instance.variationTimer = randFloat(3000.0f, 11000.0f);
instance.variationTimer = randFloat(rendering::M2_VARIATION_TIMER_MIN_MS, rendering::M2_VARIATION_TIMER_MAX_MS);
}
// Seed bone matrices from an existing instance of the same model so the
@ -199,7 +200,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4&
instance.idleSequenceIndex = 0;
instance.animDuration = static_cast<float>(mdl2.sequences[0].duration);
instance.animTime = static_cast<float>(randRange(std::max(1u, mdl2.sequences[0].duration)));
instance.variationTimer = randFloat(3000.0f, 11000.0f);
instance.variationTimer = randFloat(rendering::M2_VARIATION_TIMER_MIN_MS, rendering::M2_VARIATION_TIMER_MAX_MS);
}
// Seed bone matrices from an existing sibling so the instance renders immediately
@ -263,7 +264,9 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
// Cache camera state for frustum-culling bone computation
cachedCamPos_ = cameraPos;
const float maxRenderDistance = (instances.size() > 2000) ? 800.0f : 2800.0f;
const float maxRenderDistance = (instances.size() > rendering::M2_HIGH_DENSITY_INSTANCE_THRESHOLD)
? rendering::M2_MAX_RENDER_DISTANCE_HIGH_DENSITY
: rendering::M2_MAX_RENDER_DISTANCE_LOW_DENSITY;
cachedMaxRenderDistSq_ = maxRenderDistance * maxRenderDistance;
// Build frustum for culling bones
@ -271,10 +274,10 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
updateFrustum.extractFromMatrix(viewProjection);
// --- Smoke particle spawning (only iterate tracked smoke instances) ---
std::uniform_real_distribution<float> distXY(-0.4f, 0.4f);
std::uniform_real_distribution<float> distXY(rendering::SMOKE_OFFSET_XY_MIN, rendering::SMOKE_OFFSET_XY_MAX);
std::uniform_real_distribution<float> distVelXY(-0.3f, 0.3f);
std::uniform_real_distribution<float> distVelZ(3.0f, 5.0f);
std::uniform_real_distribution<float> distLife(4.0f, 7.0f);
std::uniform_real_distribution<float> distVelZ(rendering::SMOKE_VEL_Z_MIN, rendering::SMOKE_VEL_Z_MAX);
std::uniform_real_distribution<float> distLife(rendering::SMOKE_LIFETIME_MIN, rendering::SMOKE_LIFETIME_MAX);
std::uniform_real_distribution<float> distDrift(-0.2f, 0.2f);
smokeEmitAccum += deltaTime;
@ -287,13 +290,13 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
auto& instance = instances[si];
glm::vec3 emitWorld = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
bool spark = (smokeRng() % 8 == 0);
bool spark = (smokeRng() % rendering::SPARK_PROBABILITY_DENOM == 0);
SmokeParticle p;
p.position = emitWorld + glm::vec3(distXY(smokeRng), distXY(smokeRng), 0.0f);
if (spark) {
p.velocity = glm::vec3(distVelXY(smokeRng) * 2.0f, distVelXY(smokeRng) * 2.0f, distVelZ(smokeRng) * 1.5f);
p.maxLife = 0.8f + static_cast<float>(smokeRng() % 100) / 100.0f * 1.2f;
p.maxLife = rendering::SPARK_LIFE_BASE + static_cast<float>(smokeRng() % 100) / 100.0f * rendering::SPARK_LIFE_RANGE;
p.size = 0.5f;
p.isSpark = 1.0f;
} else {
@ -320,12 +323,12 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
continue;
}
p.position += p.velocity * deltaTime;
p.velocity.z *= 0.98f; // Slight deceleration
p.velocity.z *= rendering::SMOKE_Z_VEL_DAMPING; // Slight deceleration
p.velocity.x += distDrift(smokeRng) * deltaTime;
p.velocity.y += distDrift(smokeRng) * deltaTime;
// Grow from 1.0 to 3.5 over lifetime
float t = p.life / p.maxLife;
p.size = 1.0f + t * 2.5f;
p.size = rendering::SMOKE_SIZE_START + t * rendering::SMOKE_SIZE_GROWTH;
++i;
}
@ -389,7 +392,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
// Handle animation looping / variation transitions
if (instance.animDuration <= 0.0f && instance.cachedHasParticleEmitters) {
instance.animDuration = 3333.0f;
instance.animDuration = rendering::M2_DEFAULT_PARTICLE_ANIM_MS;
}
if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) {
if (instance.playingVariation) {
@ -399,7 +402,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
instance.animDuration = static_cast<float>(model.sequences[instance.idleSequenceIndex].duration);
}
instance.animTime = 0.0f;
instance.variationTimer = randFloat(4000.0f, 10000.0f);
instance.variationTimer = randFloat(rendering::M2_LOOP_VARIATION_TIMER_MIN_MS, rendering::M2_LOOP_VARIATION_TIMER_MAX_MS);
} else {
// Use iterative subtraction instead of fmod() to preserve precision
float duration = std::max(1.0f, instance.animDuration);
@ -421,7 +424,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
instance.animDuration = static_cast<float>(model.sequences[newSeq].duration);
instance.animTime = 0.0f;
} else {
instance.variationTimer = randFloat(2000.0f, 6000.0f);
instance.variationTimer = randFloat(rendering::M2_IDLE_VARIATION_TIMER_MIN_MS, rendering::M2_IDLE_VARIATION_TIMER_MAX_MS);
}
}
}
@ -431,21 +434,21 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
float cullRadius = worldRadius;
glm::vec3 toCam = instance.position - cachedCamPos_;
float distSq = glm::dot(toCam, toCam);
float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / 12.0f);
float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR);
if (distSq > effectiveMaxDistSq) continue;
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN);
if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue;
// LOD 3 skip: models beyond 150 units use the lowest LOD mesh which has
// no visible skeletal animation. Keep their last-computed bone matrices
// (always valid — seeded on spawn) and avoid the expensive per-bone work.
constexpr float kLOD3DistSq = 150.0f * 150.0f;
constexpr float kLOD3DistSq = rendering::M2_LOD3_DISTANCE * rendering::M2_LOD3_DISTANCE;
if (distSq > kLOD3DistSq) continue;
// Distance-based frame skipping: update distant bones less frequently
uint32_t boneInterval = 1;
if (distSq > 100.0f * 100.0f) boneInterval = 4;
else if (distSq > 50.0f * 50.0f) boneInterval = 2;
if (distSq > rendering::M2_BONE_SKIP_DIST_FAR * rendering::M2_BONE_SKIP_DIST_FAR) boneInterval = 4;
else if (distSq > rendering::M2_BONE_SKIP_DIST_MID * rendering::M2_BONE_SKIP_DIST_MID) boneInterval = 2;
instance.frameSkipCounter++;
if ((instance.frameSkipCounter % boneInterval) != 0) continue;
@ -616,9 +619,12 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c
const float* prevM = &prevVP_[0][0];
for (int k = 0; k < 16; ++k)
maxDiff = std::max(maxDiff, std::abs(curM[k] - prevM[k]));
// Threshold: typical small camera motion produces diffs < 0.05.
// A fast rotation easily exceeds 0.3. Skip HiZ when diff is large.
if (maxDiff > 0.15f) hizSafe = false;
// Threshold: typical tracking-camera motion (following a walking
// character) produces diffs of 0.050.25. A fast rotation or
// zoom easily exceeds 0.5. The previous threshold (0.15) caused
// the HiZ pass to toggle on/off every other frame during normal
// gameplay, which produced global M2 doodad flicker.
if (maxDiff > rendering::HIZ_VP_DIFF_THRESHOLD) hizSafe = false;
}
ubo->hizEnabled = hizSafe ? 1u : 0u;
@ -656,11 +662,11 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c
if (inst.cachedDisableAnimation) {
cullRadius = std::max(cullRadius, 3.0f);
}
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f);
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR);
if (inst.cachedDisableAnimation) effectiveMaxDistSq *= 2.6f;
if (inst.cachedIsGroundDetail) effectiveMaxDistSq *= 0.9f;
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN);
uint32_t flags = 0;
if (inst.cachedIsValid) flags |= 1u;
@ -668,7 +674,9 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c
if (inst.cachedIsInvisibleTrap) flags |= 4u;
// Bit 3: previouslyVisible — the shader skips HiZ for objects
// that were NOT rendered last frame (no reliable depth data).
if (i < prevFrameVisible_.size() && prevFrameVisible_[i])
// Hysteresis: treat as "previously visible" unless culled for
// 2+ consecutive frames, preventing single-frame false-cull flicker.
if (i < prevFrameVisible_.size() && prevFrameVisible_[i] < 2)
flags |= 8u;
input[i].sphere = glm::vec4(inst.position, paddedRadius);
@ -756,15 +764,25 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
// Snapshot the GPU visibility results into prevFrameVisible_ so the NEXT
// frame's compute dispatch can set the per-instance `previouslyVisible`
// flag (bit 3). Objects not visible this frame will skip HiZ next frame,
// avoiding false culls from stale depth data.
// flag (bit 3). We use a hysteresis counter instead of a binary flag to
// prevent a 1-frame-on / 1-frame-off oscillation: an object must be HiZ-
// culled for 2 consecutive frames before we stop considering it
// "previously visible". This eliminates doodad flicker near characters
// caused by stale depth data from character movement.
if (gpuCullAvailable) {
prevFrameVisible_.resize(numInstances);
for (uint32_t i = 0; i < numInstances; ++i)
prevFrameVisible_[i] = visibility[i] ? 1u : 0u;
prevFrameVisible_.resize(numInstances, 0);
for (uint32_t i = 0; i < numInstances; ++i) {
if (visibility[i]) {
// Visible this frame — reset cull counter.
prevFrameVisible_[i] = 0;
} else {
// Culled this frame — increment counter (cap at 3 to avoid overflow).
prevFrameVisible_[i] = std::min<uint8_t>(prevFrameVisible_[i] + 1, 3);
}
}
} else {
// No GPU cull data — conservatively mark all as visible
prevFrameVisible_.assign(static_cast<size_t>(instances.size()), 1u);
// No GPU cull data — conservatively mark all as visible (counter = 0).
prevFrameVisible_.assign(static_cast<size_t>(instances.size()), 0);
}
// If GPU culling was not dispatched, fallback: compute distances on CPU
@ -818,12 +836,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
float worldRadius = instance.cachedBoundRadius * instance.scale;
float cullRadius = worldRadius;
if (instance.cachedDisableAnimation) cullRadius = std::max(cullRadius, 3.0f);
float effDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f);
float effDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR);
if (instance.cachedDisableAnimation) effDistSq *= 2.6f;
if (instance.cachedIsGroundDetail) effDistSq *= 0.9f;
if (distSqTest > effDistSq) continue;
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN);
if (cullRadius > 0.0f && !frustum.intersectsSphere(instance.position, paddedRadius)) continue;
}
@ -833,7 +851,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
float worldRadius = instance.cachedBoundRadius * instance.scale;
float cullRadius = worldRadius;
if (instance.cachedDisableAnimation) cullRadius = std::max(cullRadius, 3.0f);
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f);
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR);
if (instance.cachedDisableAnimation) effectiveMaxDistSq *= 2.6f;
if (instance.cachedIsGroundDetail) effectiveMaxDistSq *= 0.9f;

View file

@ -531,19 +531,24 @@ bool Renderer::initialize(core::Window* win) {
lensFlare = nullptr;
weather = std::make_unique<Weather>();
weather->initialize(vkCtx, perFrameSetLayout);
if (!weather->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Weather effect initialization failed (non-fatal)");
lightning = std::make_unique<Lightning>();
lightning->initialize(vkCtx, perFrameSetLayout);
if (!lightning->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Lightning effect initialization failed (non-fatal)");
swimEffects = std::make_unique<SwimEffects>();
swimEffects->initialize(vkCtx, perFrameSetLayout);
if (!swimEffects->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Swim effect initialization failed (non-fatal)");
mountDust = std::make_unique<MountDust>();
mountDust->initialize(vkCtx, perFrameSetLayout);
if (!mountDust->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Mount dust effect initialization failed (non-fatal)");
chargeEffect = std::make_unique<ChargeEffect>();
chargeEffect->initialize(vkCtx, perFrameSetLayout);
if (!chargeEffect->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Charge effect initialization failed (non-fatal)");
levelUpEffect = std::make_unique<LevelUpEffect>();
@ -1451,7 +1456,8 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) {
if (audioCoordinator_->getMovementSoundManager()) audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager);
break;
case 5:
if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager);
if (questMarkerRenderer && !questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager))
LOG_WARNING("Quest marker renderer re-init failed (non-fatal)");
break;
default:
deferredWorldInitPending_ = false;
@ -1930,7 +1936,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
// Create M2, WMO, and Character renderers
if (!m2Renderer) {
m2Renderer = std::make_unique<M2Renderer>();
m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager);
if (!m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager))
LOG_ERROR("M2Renderer initialization failed");
if (swimEffects) {
swimEffects->setM2Renderer(m2Renderer.get());
}
@ -1959,21 +1966,26 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
}
if (!wmoRenderer) {
wmoRenderer = std::make_unique<WMORenderer>();
wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager);
if (!wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager))
LOG_ERROR("WMORenderer initialization failed");
if (shadowRenderPass != VK_NULL_HANDLE) {
wmoRenderer->initializeShadow(shadowRenderPass);
if (!wmoRenderer->initializeShadow(shadowRenderPass))
LOG_WARNING("WMO shadow pipeline initialization failed");
}
}
// Initialize shadow pipelines for M2 if not yet done
if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE && !m2Renderer->hasShadowPipeline()) {
m2Renderer->initializeShadow(shadowRenderPass);
if (!m2Renderer->initializeShadow(shadowRenderPass))
LOG_WARNING("M2 shadow pipeline initialization failed");
}
if (!characterRenderer) {
characterRenderer = std::make_unique<CharacterRenderer>();
characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager);
if (!characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager))
LOG_ERROR("CharacterRenderer initialization failed");
if (shadowRenderPass != VK_NULL_HANDLE) {
characterRenderer->initializeShadow(shadowRenderPass);
if (!characterRenderer->initializeShadow(shadowRenderPass))
LOG_WARNING("Character shadow pipeline initialization failed");
}
}
@ -2068,7 +2080,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
audioCoordinator_->getMovementSoundManager()->initialize(assetManager);
}
if (questMarkerRenderer) {
questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager);
if (!questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager))
LOG_WARNING("Quest marker renderer initialization failed (non-fatal)");
}
if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) {
@ -2261,7 +2274,8 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent
audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager);
}
if (questMarkerRenderer && cachedAssetManager) {
questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager);
if (!questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager))
LOG_WARNING("Quest marker renderer re-init failed (non-fatal)");
}
} else {
deferredWorldInitPending_ = true;

View file

@ -868,7 +868,8 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
// Ensure M2 renderer has asset manager
if (m2Renderer && assetManager) {
m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
if (!m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager))
LOG_WARNING("M2Renderer terrain re-init failed");
}
ft.phase = FinalizationPhase::M2_MODELS;
@ -952,7 +953,8 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
case FinalizationPhase::WMO_MODELS: {
// Upload multiple WMO models per call (batched GPU uploads)
if (wmoRenderer && assetManager) {
wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
if (!wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager))
LOG_WARNING("WMORenderer terrain re-init failed");
// Set pre-decoded BLP cache and defer normal maps during streaming
wmoRenderer->setPredecodedBLPCache(&pending->preloadedWMOTextures);
wmoRenderer->setDeferNormalMaps(true);