#include "rendering/m2_model_classifier.hpp" #include #include #include #include namespace wowee { namespace rendering { namespace { // Returns true if `lower` contains `token` as a substring. // Caller must provide an already-lowercased string. inline bool has(const std::string& lower, std::string_view token) noexcept { return lower.find(token) != std::string::npos; } // Returns true if any token in the compile-time array is a substring of `lower`. template bool hasAny(const std::string& lower, const std::array& tokens) noexcept { for (auto tok : tokens) if (lower.find(tok) != std::string::npos) return true; return false; } } // namespace M2ClassificationResult classifyM2Model( const std::string& name, const glm::vec3& boundsMin, const glm::vec3& boundsMax, std::size_t vertexCount, std::size_t emitterCount) { // Single lowercased copy — all token checks share it. std::string n = name; std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); M2ClassificationResult r; // --------------------------------------------------------------- // Geometry metrics // --------------------------------------------------------------- const glm::vec3 dims = boundsMax - boundsMin; const float horiz = std::max(dims.x, dims.y); const float vert = std::max(0.0f, dims.z); const bool lowWide = (horiz > 1.4f && vert > 0.2f && vert < horiz * 0.70f); const bool lowPlat = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); // --------------------------------------------------------------- // Simple single-token flags // --------------------------------------------------------------- r.isInvisibleTrap = has(n, "invisibletrap"); r.isGroundDetail = has(n, "\\nodxt\\detail\\") || has(n, "\\detail\\"); r.isSmoke = has(n, "smoke"); r.isLavaModel = has(n, "forgelava") || has(n, "lavapot") || has(n, "lavaflow"); r.isInstancePortal = has(n, "instanceportal") || has(n, "instancenewportal") || has(n, "portalfx") || has(n, "spellportal"); r.isWaterVegetation = has(n, "cattail") || has(n, "reed") || has(n, "bulrush") || has(n, "seaweed") || has(n, "kelp") || has(n, "lilypad"); r.isElvenLike = has(n, "elf") || has(n, "elven") || has(n, "quel"); r.isLanternLike = has(n, "lantern") || has(n, "lamp") || has(n, "light"); r.isKoboldFlame = has(n, "kobold") && (has(n, "candle") || has(n, "torch") || has(n, "mine")); // --------------------------------------------------------------- // Collision: shape categories (mirrors original logic ordering) // --------------------------------------------------------------- const bool isPlanter = has(n, "planter"); const bool likelyCurb = isPlanter || has(n, "curb") || has(n, "base") || has(n, "ring") || has(n, "well"); const bool knownSwPlanter = has(n, "stormwindplanter") || has(n, "stormwindwindowplanter"); const bool bridgeName = has(n, "bridge") || has(n, "plank") || has(n, "walkway"); const bool statueName = has(n, "statue") || has(n, "monument") || has(n, "sculpture"); const bool sittable = has(n, "chair") || has(n, "bench") || has(n, "stool") || has(n, "seat") || has(n, "throne"); const bool smallSolid = (statueName && !sittable) || has(n, "crate") || has(n, "box") || has(n, "chest") || has(n, "barrel"); const bool chestName = has(n, "chest"); r.collisionSteppedFountain = has(n, "fountain"); r.collisionSteppedLowPlatform = !r.collisionSteppedFountain && (knownSwPlanter || bridgeName || (likelyCurb && (lowPlat || lowWide))); r.collisionBridge = bridgeName; r.collisionPlanter = isPlanter; r.collisionStatue = statueName; const bool narrowVertName = has(n, "lamp") || has(n, "lantern") || has(n, "post") || has(n, "pole"); const bool narrowVertShape = (horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f); r.collisionNarrowVerticalProp = !r.collisionSteppedFountain && !r.collisionSteppedLowPlatform && (narrowVertName || narrowVertShape); // --------------------------------------------------------------- // Foliage token table (sorted alphabetically) // --------------------------------------------------------------- static constexpr auto kFoliageTokens = std::to_array({ "algae", "bamboo", "banana", "branch", "bush", "cactus", "canopy", "cattail", "coconut", "coral", "corn", "crop", "dead-grass", "dead_grass", "deadgrass", "dry-grass", "dry_grass", "drygrass", "fern", "fireflies", "firefly", "fireflys", "flower", "frond", "fungus", "gourd", "grass", "hay", "hedge", "ivy", "kelp", "leaf", "leaves", "lily", "melon", "moss", "mushroom", "palm", "pumpkin", "reed", "root", "seaweed", "shrub", "squash", "stalk", "thorn", "toadstool", "vine", "watermelon", "weed", "wheat", }); // "plant" is foliage unless "planter" is also present (planters are solid curbs). const bool foliagePlant = has(n, "plant") && !isPlanter; const bool foliageName = foliagePlant || hasAny(n, kFoliageTokens); const bool treeLike = has(n, "tree"); const bool hardTreePart = has(n, "trunk") || has(n, "stump") || has(n, "log"); // Trees wide/tall enough to have a visible trunk → solid cylinder collision. const bool treeWithTrunk = treeLike && !hardTreePart && !foliageName && horiz > 6.0f && vert > 4.0f; const bool softTree = treeLike && !hardTreePart && !treeWithTrunk; r.collisionTreeTrunk = treeWithTrunk; const bool genericSolid = (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f) || statueName; const bool curbLikeName = has(n, "curb") || has(n, "planter") || has(n, "ring") || has(n, "well") || has(n, "base"); const bool lowPlatLikeShape = lowWide || lowPlat; r.collisionSmallSolidProp = !r.collisionSteppedFountain && !r.collisionSteppedLowPlatform && !r.collisionNarrowVerticalProp && !r.collisionTreeTrunk && !curbLikeName && !lowPlatLikeShape && (smallSolid || (genericSolid && !foliageName && !softTree)); const bool carpetOrRug = has(n, "carpet") || has(n, "rug"); const bool forceSolidCurb = r.collisionSteppedLowPlatform || knownSwPlanter || likelyCurb || r.collisionPlanter; r.collisionNoBlock = (foliageName || softTree || carpetOrRug) && !forceSolidCurb; // Ground-clutter detail cards are always non-blocking. if (r.isGroundDetail) r.collisionNoBlock = true; // --------------------------------------------------------------- // Ambient creatures: fireflies, dragonflies, moths, butterflies // --------------------------------------------------------------- static constexpr auto kAmbientTokens = std::to_array({ "butterfly", "dragonflies", "dragonfly", "fireflies", "firefly", "fireflys", "moth", }); const bool ambientCreature = hasAny(n, kAmbientTokens); // --------------------------------------------------------------- // Animation / foliage rendering flags // --------------------------------------------------------------- const bool foliageOrTree = foliageName || treeLike; r.isFoliageLike = foliageOrTree && !ambientCreature; r.disableAnimation = r.isFoliageLike || chestName; r.shadowWindFoliage = r.isFoliageLike; r.isFireflyEffect = ambientCreature; // --------------------------------------------------------------- // Spell effects (named tokens + particle-dominated geometry heuristic) // --------------------------------------------------------------- static constexpr auto kEffectTokens = std::to_array({ "bubbles", "hazardlight", "instancenewportal", "instanceportal", "lavabubble", "lavasplash", "lavasteam", "levelup", "lightshaft", "mageportal", "particleemitter", "spotlight", "volumetriclight", "wisps", "worldtreeportal", }); r.isSpellEffect = hasAny(n, kEffectTokens) || (emitterCount >= 3 && vertexCount <= 200); // Instance portals are spell effects too. if (r.isInstancePortal) r.isSpellEffect = true; return r; } // --------------------------------------------------------------------------- // classifyBatchTexture // --------------------------------------------------------------------------- M2BatchTexClassification classifyBatchTexture(const std::string& lowerTexKey) { M2BatchTexClassification r; // Exact paths for well-known lantern / lamp glow-card textures. static constexpr auto kExactGlowTextures = std::to_array({ "world\\azeroth\\karazahn\\passivedoodads\\bonfire\\flamelicksmallblue.blp", "world\\expansion06\\doodads\\nightelf\\7ne_druid_streetlamp01_light.blp", "world\\generic\\human\\passive doodads\\stormwind\\t_vfx_glow01_64.blp", "world\\generic\\nightelf\\passive doodads\\lamps\\glowblue32.blp", "world\\generic\\nightelf\\passive doodads\\magicalimplements\\glow.blp", }); for (auto s : kExactGlowTextures) if (lowerTexKey == s) { r.exactLanternGlowTex = true; break; } static constexpr auto kGlowTokens = std::to_array({ "flare", "glow", "halo", "light", }); static constexpr auto kFlameTokens = std::to_array({ "ember", "fire", "flame", "flamelick", }); static constexpr auto kGlowCardTokens = std::to_array({ "flamelick", "genericglow", "glow", "glowball", "lensflare", "lightbeam", "t_vfx", }); static constexpr auto kLikelyFlameTokens = std::to_array({ "fire", "flame", "torch", }); static constexpr auto kLanternFamilyTokens = std::to_array({ "elf", "lamp", "lantern", "quel", "silvermoon", "thalas", }); static constexpr auto kCoolTintTokens = std::to_array({ "arcane", "blue", "nightelf", }); static constexpr auto kRedTintTokens = std::to_array({ "red", "ruby", "scarlet", }); r.hasGlowToken = hasAny(lowerTexKey, kGlowTokens); r.hasFlameToken = hasAny(lowerTexKey, kFlameTokens); r.hasGlowCardToken = hasAny(lowerTexKey, kGlowCardTokens); r.likelyFlame = hasAny(lowerTexKey, kLikelyFlameTokens); r.lanternFamily = hasAny(lowerTexKey, kLanternFamilyTokens); r.glowTint = hasAny(lowerTexKey, kCoolTintTokens) ? 1 : hasAny(lowerTexKey, kRedTintTokens) ? 2 : 0; return r; } } // namespace rendering } // namespace wowee