#include "cli_wom_info.hpp" #include "cli_arg_parse.hpp" #include "pipeline/wowee_model.hpp" #include "pipeline/wowee_building.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/asset_manager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { int handleInfo(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); // Allow either "/path/to/file.wom" or "/path/to/file"; load() expects no extension. if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeModelLoader::exists(base)) { std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); return 1; } auto wom = wowee::pipeline::WoweeModelLoader::load(base); if (jsonOut) { nlohmann::json j; j["wom"] = base + ".wom"; j["version"] = wom.version; j["name"] = wom.name; j["vertices"] = wom.vertices.size(); j["indices"] = wom.indices.size(); j["triangles"] = wom.indices.size() / 3; j["textures"] = wom.texturePaths.size(); j["bones"] = wom.bones.size(); j["animations"] = wom.animations.size(); j["batches"] = wom.batches.size(); j["boundRadius"] = wom.boundRadius; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WOM: %s.wom\n", base.c_str()); std::printf(" version : %u%s\n", wom.version, wom.version == 3 ? " (multi-batch)" : wom.version == 2 ? " (animated)" : " (static)"); std::printf(" name : %s\n", wom.name.c_str()); std::printf(" vertices : %zu\n", wom.vertices.size()); std::printf(" indices : %zu (%zu tris)\n", wom.indices.size(), wom.indices.size() / 3); std::printf(" textures : %zu\n", wom.texturePaths.size()); std::printf(" bones : %zu\n", wom.bones.size()); std::printf(" animations : %zu\n", wom.animations.size()); std::printf(" batches : %zu\n", wom.batches.size()); std::printf(" boundRadius: %.2f\n", wom.boundRadius); return 0; } int handleInfoBatches(int& i, int argc, char** argv) { // Per-batch breakdown of a WOM3 (multi-material) model. // --info shows the total batch count; this drills into each // one's index range, texture, blend mode, and flags. Useful // for debugging 'why is this submesh transparent?' or // 'which batch has the bad UV?'. std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeModelLoader::exists(base)) { std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); return 1; } auto wom = wowee::pipeline::WoweeModelLoader::load(base); // Blend modes per WoweeModel::Batch comment: // 0=opaque, 1=alpha-test, 2=alpha, 3=add auto blendName = [](uint16_t b) { switch (b) { case 0: return "opaque"; case 1: return "alpha-test"; case 2: return "alpha"; case 3: return "add"; } return "?"; }; // Flags bits: // bit 0 (0x01) = unlit // bit 1 (0x02) = two-sided // bit 2 (0x04) = no z-write auto flagsStr = [](uint16_t f) { std::string s; if (f & 0x01) s += "unlit "; if (f & 0x02) s += "two-sided "; if (f & 0x04) s += "no-zwrite "; if (s.empty()) s = "-"; else s.pop_back(); // drop trailing space return s; }; if (jsonOut) { nlohmann::json j; j["wom"] = base + ".wom"; j["version"] = wom.version; j["totalBatches"] = wom.batches.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < wom.batches.size(); ++k) { const auto& b = wom.batches[k]; std::string tex = (b.textureIndex < wom.texturePaths.size()) ? wom.texturePaths[b.textureIndex] : std::string(""); arr.push_back({ {"index", k}, {"indexStart", b.indexStart}, {"indexCount", b.indexCount}, {"triangles", b.indexCount / 3}, {"textureIndex", b.textureIndex}, {"texturePath", tex}, {"blendMode", b.blendMode}, {"blendName", blendName(b.blendMode)}, {"flags", b.flags}, {"flagsStr", flagsStr(b.flags)}, }); } j["batches"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WOM batches: %s.wom (v%u, %zu batches)\n", base.c_str(), wom.version, wom.batches.size()); if (wom.batches.empty()) { std::printf(" *no batches (WOM1/WOM2 single-material model)*\n"); return 0; } std::printf(" idx iStart iCount tris blend flags texture\n"); for (size_t k = 0; k < wom.batches.size(); ++k) { const auto& b = wom.batches[k]; std::string tex = (b.textureIndex < wom.texturePaths.size()) ? wom.texturePaths[b.textureIndex] : std::string(""); std::printf(" %3zu %6u %6u %5u %-10s %-13s %s\n", k, b.indexStart, b.indexCount, b.indexCount / 3, blendName(b.blendMode), flagsStr(b.flags).c_str(), tex.c_str()); } return 0; } int handleInfoTextures(int& i, int argc, char** argv) { // List every texture path a WOM references, with on-disk // presence for both BLP (proprietary) and PNG (sidecar) // forms. Useful for tracking which textures are missing // before --pack-wcp would fail at runtime. std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeModelLoader::exists(base)) { std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); return 1; } auto wom = wowee::pipeline::WoweeModelLoader::load(base); namespace fs = std::filesystem; // Texture paths in WOMs are usually game-relative // ('World/Generic/Tree.blp'); resolve them against the // common Data/ root for the on-disk presence check. Skip // the check when the path doesn't exist as either an // absolute or relative file (avoids false 'missing' // reports when the user runs from outside the data root). auto checkBlp = [&](const std::string& p) { if (fs::exists(p)) return true; std::string lower = p; for (auto& c : lower) c = std::tolower(static_cast(c)); if (lower.size() < 4 || lower.substr(lower.size() - 4) != ".blp") { lower += ".blp"; } return fs::exists("Data/" + lower); }; auto sidecarPng = [&](const std::string& p) { std::string base = p; if (base.size() >= 4 && (base.substr(base.size() - 4) == ".blp" || base.substr(base.size() - 4) == ".BLP")) { base = base.substr(0, base.size() - 4); } std::string png = base + ".png"; if (fs::exists(png)) return true; std::string lower = png; for (auto& c : lower) c = std::tolower(static_cast(c)); return fs::exists("Data/" + lower); }; if (jsonOut) { nlohmann::json j; j["wom"] = base + ".wom"; j["textureCount"] = wom.texturePaths.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < wom.texturePaths.size(); ++k) { const auto& p = wom.texturePaths[k]; arr.push_back({ {"index", k}, {"path", p}, {"blpPresent", checkBlp(p)}, {"pngPresent", sidecarPng(p)}, }); } j["textures"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WOM textures: %s.wom (%zu textures)\n", base.c_str(), wom.texturePaths.size()); if (wom.texturePaths.empty()) { std::printf(" *no texture references*\n"); return 0; } std::printf(" idx blp png path\n"); for (size_t k = 0; k < wom.texturePaths.size(); ++k) { const auto& p = wom.texturePaths[k]; std::printf(" %3zu %s %s %s\n", k, checkBlp(p) ? "y" : "-", sidecarPng(p) ? "y" : "-", p.c_str()); } return 0; } int handleInfoDoodads(int& i, int argc, char** argv) { // List every doodad placement in a WOB (M2 instances inside // a building). Companion to --info-textures: where one // tracks GPU resources, this tracks scene composition. std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); if (base.size() >= 4 && base.substr(base.size() - 4) == ".wob") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeBuildingLoader::exists(base)) { std::fprintf(stderr, "WOB not found: %s.wob\n", base.c_str()); return 1; } auto bld = wowee::pipeline::WoweeBuildingLoader::load(base); if (jsonOut) { nlohmann::json j; j["wob"] = base + ".wob"; j["count"] = bld.doodads.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < bld.doodads.size(); ++k) { const auto& d = bld.doodads[k]; arr.push_back({ {"index", k}, {"modelPath", d.modelPath}, {"position", {d.position.x, d.position.y, d.position.z}}, {"rotation", {d.rotation.x, d.rotation.y, d.rotation.z}}, {"scale", d.scale}, }); } j["doodads"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("WOB doodads: %s.wob (%zu placements)\n", base.c_str(), bld.doodads.size()); if (bld.doodads.empty()) { std::printf(" *no doodad placements*\n"); return 0; } std::printf(" idx scale pos (x, y, z) rot (x, y, z) model\n"); for (size_t k = 0; k < bld.doodads.size(); ++k) { const auto& d = bld.doodads[k]; std::printf(" %3zu %5.2f (%6.1f, %6.1f, %6.1f) (%6.1f, %6.1f, %6.1f) %s\n", k, d.scale, d.position.x, d.position.y, d.position.z, d.rotation.x, d.rotation.y, d.rotation.z, d.modelPath.c_str()); } return 0; } int handleInfoAttachParticleSequence(int& i, int argc, char** argv) { // Three M2 inspectors share an entry point — they all need // the same M2Loader::load + skin merge dance, then differ // only in which sub-array they iterate. enum Kind { kAttach, kParticle, kSequence }; Kind kind; const char* cmdName; if (std::strcmp(argv[i], "--info-attachments") == 0) { kind = kAttach; cmdName = "info-attachments"; } else if (std::strcmp(argv[i], "--info-particles") == 0) { kind = kParticle; cmdName = "info-particles"; } else { kind = kSequence; cmdName = "info-sequences"; } std::string path = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); std::ifstream in(path, std::ios::binary); if (!in) { std::fprintf(stderr, "%s: cannot open %s\n", cmdName, path.c_str()); return 1; } std::vector bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); // Auto-merge skin for vertex/index counts to match render. std::vector skinBytes; { std::string skinPath = path; auto dot = skinPath.rfind('.'); if (dot != std::string::npos) skinPath = skinPath.substr(0, dot) + "00.skin"; std::ifstream sf(skinPath, std::ios::binary); if (sf) { skinBytes.assign((std::istreambuf_iterator(sf)), std::istreambuf_iterator()); } } auto m2 = wowee::pipeline::M2Loader::load(bytes); if (!skinBytes.empty()) { wowee::pipeline::M2Loader::loadSkin(skinBytes, m2); } if (kind == kAttach) { if (jsonOut) { nlohmann::json j; j["m2"] = path; j["count"] = m2.attachments.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < m2.attachments.size(); ++k) { const auto& a = m2.attachments[k]; arr.push_back({ {"index", k}, {"id", a.id}, {"bone", a.bone}, {"position", {a.position.x, a.position.y, a.position.z}} }); } j["attachments"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("M2 attachments: %s (%zu)\n", path.c_str(), m2.attachments.size()); if (m2.attachments.empty()) { std::printf(" *no attachments*\n"); return 0; } std::printf(" idx id bone pos (x, y, z)\n"); for (size_t k = 0; k < m2.attachments.size(); ++k) { const auto& a = m2.attachments[k]; std::printf(" %3zu %3u %4u (%6.2f, %6.2f, %6.2f)\n", k, a.id, a.bone, a.position.x, a.position.y, a.position.z); } return 0; } if (kind == kParticle) { auto blendName = [](uint8_t b) { switch (b) { case 0: return "opaque"; case 1: return "alphakey"; case 2: return "alpha"; case 4: return "add"; } return "?"; }; if (jsonOut) { nlohmann::json j; j["m2"] = path; j["particleEmitters"] = m2.particleEmitters.size(); j["ribbonEmitters"] = m2.ribbonEmitters.size(); nlohmann::json parts = nlohmann::json::array(); for (size_t k = 0; k < m2.particleEmitters.size(); ++k) { const auto& p = m2.particleEmitters[k]; parts.push_back({ {"index", k}, {"particleId", p.particleId}, {"bone", p.bone}, {"texture", p.texture}, {"blendingType", p.blendingType}, {"blendName", blendName(p.blendingType)}, {"emitterType", p.emitterType}, {"position", {p.position.x, p.position.y, p.position.z}} }); } j["particles"] = parts; nlohmann::json ribbons = nlohmann::json::array(); for (size_t k = 0; k < m2.ribbonEmitters.size(); ++k) { const auto& r = m2.ribbonEmitters[k]; ribbons.push_back({ {"index", k}, {"ribbonId", r.ribbonId}, {"bone", r.bone}, {"textureIndex", r.textureIndex}, {"materialIndex", r.materialIndex}, {"position", {r.position.x, r.position.y, r.position.z}} }); } j["ribbons"] = ribbons; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("M2 emitters: %s\n", path.c_str()); std::printf(" particles: %zu, ribbons: %zu\n", m2.particleEmitters.size(), m2.ribbonEmitters.size()); if (!m2.particleEmitters.empty()) { std::printf("\n Particles:\n"); std::printf(" idx id bone tex blend type pos (x, y, z)\n"); for (size_t k = 0; k < m2.particleEmitters.size(); ++k) { const auto& p = m2.particleEmitters[k]; std::printf(" %3zu %3d %4u %3u %-8s %4u (%5.1f, %5.1f, %5.1f)\n", k, p.particleId, p.bone, p.texture, blendName(p.blendingType), p.emitterType, p.position.x, p.position.y, p.position.z); } } if (!m2.ribbonEmitters.empty()) { std::printf("\n Ribbons:\n"); std::printf(" idx id bone tex mat pos (x, y, z)\n"); for (size_t k = 0; k < m2.ribbonEmitters.size(); ++k) { const auto& r = m2.ribbonEmitters[k]; std::printf(" %3zu %3d %4u %3u %3u (%5.1f, %5.1f, %5.1f)\n", k, r.ribbonId, r.bone, r.textureIndex, r.materialIndex, r.position.x, r.position.y, r.position.z); } } return 0; } // kind == kSequence if (jsonOut) { nlohmann::json j; j["m2"] = path; j["count"] = m2.sequences.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < m2.sequences.size(); ++k) { const auto& s = m2.sequences[k]; arr.push_back({ {"index", k}, {"id", s.id}, {"variation", s.variationIndex}, {"durationMs", s.duration}, {"flags", s.flags}, {"movingSpeed", s.movingSpeed}, {"frequency", s.frequency}, {"blendTimeMs", s.blendTime} }); } j["sequences"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("M2 sequences: %s (%zu)\n", path.c_str(), m2.sequences.size()); if (m2.sequences.empty()) { std::printf(" *no sequences*\n"); return 0; } std::printf(" idx id var duration flags speed blend\n"); for (size_t k = 0; k < m2.sequences.size(); ++k) { const auto& s = m2.sequences[k]; std::printf(" %3zu %3u %3u %8u %5u %5.2f %5u\n", k, s.id, s.variationIndex, s.duration, s.flags, s.movingSpeed, s.blendTime); } return 0; } int handleInfoBones(int& i, int argc, char** argv) { // Inspect M2 bone tree. Shows parent index, key-bone ID // (-1 if not a named bone), pivot offset, and a depth // indicator computed by walking up parents — useful for // debugging skeleton structure when something looks wrong // in the renderer ('why is this bone not following its parent?'). std::string path = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); std::ifstream in(path, std::ios::binary); if (!in) { std::fprintf(stderr, "info-bones: cannot open %s\n", path.c_str()); return 1; } std::vector bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); auto m2 = wowee::pipeline::M2Loader::load(bytes); // Compute depth per bone — guard against cycles by capping // walk length at boneCount (a real DAG can't exceed that). std::vector depths(m2.bones.size(), -1); for (size_t k = 0; k < m2.bones.size(); ++k) { int d = 0; int idx = static_cast(k); while (idx >= 0 && d <= static_cast(m2.bones.size())) { int parent = m2.bones[idx].parentBone; if (parent < 0) break; idx = parent; d++; } depths[k] = d; } if (jsonOut) { nlohmann::json j; j["m2"] = path; j["count"] = m2.bones.size(); nlohmann::json arr = nlohmann::json::array(); for (size_t k = 0; k < m2.bones.size(); ++k) { const auto& b = m2.bones[k]; arr.push_back({ {"index", k}, {"keyBoneId", b.keyBoneId}, {"parent", b.parentBone}, {"flags", b.flags}, {"depth", depths[k]}, {"pivot", {b.pivot.x, b.pivot.y, b.pivot.z}} }); } j["bones"] = arr; std::printf("%s\n", j.dump(2).c_str()); return 0; } std::printf("M2 bones: %s (%zu)\n", path.c_str(), m2.bones.size()); if (m2.bones.empty()) { std::printf(" *no bones (static model)*\n"); return 0; } std::printf(" idx parent depth keyBone flags pivot (x, y, z)\n"); for (size_t k = 0; k < m2.bones.size(); ++k) { const auto& b = m2.bones[k]; // Indent the keyBone column by depth so the tree shape // is visible at a glance. std::printf(" %3zu %6d %5d %7d %5u (%6.2f, %6.2f, %6.2f)\n", k, b.parentBone, depths[k], b.keyBoneId, b.flags, b.pivot.x, b.pivot.y, b.pivot.z); } return 0; } int handleValidateWom(int& i, int argc, char** argv) { // Static sanity checks on a .wom: catches malformed // hand-built or import-corrupted models before they reach // the renderer (where errors usually crash or render blank). // Mirrors --validate-wol / --validate-wow. Reports each // failed check with details and exits non-zero on any // failure; clean models print a single OK line. std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeModelLoader::exists(base)) { std::fprintf(stderr, "validate-wom: WOM not found: %s.wom\n", base.c_str()); return 1; } auto wom = wowee::pipeline::WoweeModelLoader::load(base); std::vector errors; std::vector warnings; // 1) version field if (wom.version < 1 || wom.version > 3) { errors.push_back("version " + std::to_string(wom.version) + " not in supported range 1-3"); } // 2) non-empty geometry if (wom.vertices.empty()) errors.push_back("vertex list is empty"); if (wom.indices.empty()) errors.push_back("index list is empty"); if (wom.indices.size() % 3 != 0) { errors.push_back("index count " + std::to_string(wom.indices.size()) + " is not a multiple of 3 (not a triangle list)"); } // 3) all indices < vertex count uint32_t vCount = static_cast(wom.vertices.size()); size_t oobIdx = 0; for (uint32_t idx : wom.indices) { if (idx >= vCount) { oobIdx++; } } if (oobIdx > 0) { errors.push_back(std::to_string(oobIdx) + " triangle indices reference out-of-range vertices"); } // 4) bone refs (only meaningful when bones exist) if (!wom.bones.empty()) { size_t oobBoneIdx = 0; size_t badWeightSum = 0; for (const auto& v : wom.vertices) { int sum = 0; for (int k = 0; k < 4; ++k) { if (v.boneWeights[k] > 0 && v.boneIndices[k] >= wom.bones.size()) { oobBoneIdx++; } sum += v.boneWeights[k]; } // Allow either 0 (no skinning) or 255 (full skinning, // possibly split across slots). Anything else is a // weight-table mistake. if (sum != 0 && sum != 255) { badWeightSum++; } } if (oobBoneIdx > 0) { errors.push_back(std::to_string(oobBoneIdx) + " vertex bone-index slots reference out-of-range bones"); } if (badWeightSum > 0) { warnings.push_back(std::to_string(badWeightSum) + " vertices have boneWeights summing to neither 0 nor 255"); } // parentBone < bones.size() (or -1 for root) size_t oobParent = 0; for (const auto& b : wom.bones) { if (b.parentBone >= 0 && b.parentBone >= static_cast(wom.bones.size())) { oobParent++; } } if (oobParent > 0) { errors.push_back(std::to_string(oobParent) + " bones reference out-of-range parent bones"); } } // 5) WOM3 batch coverage: union of all batch ranges should // equal [0, indices.size()) without gaps or overlaps, and // each batch.textureIndex must be a valid index. if (wom.hasBatches()) { size_t oobTex = 0, oobRange = 0; for (const auto& b : wom.batches) { if (!wom.texturePaths.empty() && b.textureIndex >= wom.texturePaths.size()) { oobTex++; } if (static_cast(b.indexStart) + b.indexCount > wom.indices.size()) { oobRange++; } } if (oobTex > 0) { errors.push_back(std::to_string(oobTex) + " batch.textureIndex values out of range"); } if (oobRange > 0) { errors.push_back(std::to_string(oobRange) + " batches index past end of index buffer"); } // Coverage check via bytemap of triangles. size_t triCount = wom.indices.size() / 3; std::vector covered(triCount, 0); for (const auto& b : wom.batches) { uint32_t tStart = b.indexStart / 3; uint32_t tEnd = (b.indexStart + b.indexCount) / 3; for (uint32_t t = tStart; t < tEnd && t < triCount; ++t) covered[t]++; } size_t uncovered = 0, overlapped = 0; for (auto c : covered) { if (c == 0) uncovered++; else if (c > 1) overlapped++; } if (uncovered > 0) { warnings.push_back(std::to_string(uncovered) + " triangles not covered by any batch"); } if (overlapped > 0) { warnings.push_back(std::to_string(overlapped) + " triangles covered by multiple batches"); } } // 6) bounds sanity if (wom.boundRadius <= 0) { warnings.push_back("boundRadius <= 0 (model will fail frustum culling)"); } if (wom.boundMin.x > wom.boundMax.x || wom.boundMin.y > wom.boundMax.y || wom.boundMin.z > wom.boundMax.z) { errors.push_back("boundMin > boundMax on at least one axis (inverted AABB)"); } // 7) animation sanity (WOM2/3): per-bone keyframe arrays // must have one entry per bone in the model. for (size_t a = 0; a < wom.animations.size(); ++a) { const auto& anim = wom.animations[a]; if (!wom.bones.empty() && anim.boneKeyframes.size() != wom.bones.size()) { errors.push_back("animation " + std::to_string(a) + " (id=" + std::to_string(anim.id) + ") has " + std::to_string(anim.boneKeyframes.size()) + " bone tracks but model has " + std::to_string(wom.bones.size()) + " bones"); } } bool ok = errors.empty(); if (jsonOut) { nlohmann::json j; j["wom"] = base + ".wom"; j["ok"] = ok; j["errors"] = errors; j["warnings"] = warnings; std::printf("%s\n", j.dump(2).c_str()); return ok ? 0 : 1; } std::printf("validate-wom: %s.wom\n", base.c_str()); if (ok && warnings.empty()) { std::printf(" OK — %zu vertices, %zu triangles, %zu batches, %zu bones, %zu animations\n", wom.vertices.size(), wom.indices.size() / 3, wom.batches.size(), wom.bones.size(), wom.animations.size()); return 0; } if (!warnings.empty()) { std::printf(" warnings (%zu):\n", warnings.size()); for (const auto& w : warnings) std::printf(" - %s\n", w.c_str()); } if (!errors.empty()) { std::printf(" ERRORS (%zu):\n", errors.size()); for (const auto& e : errors) std::printf(" - %s\n", e.c_str()); } return ok ? 0 : 1; } int handleExportBonesDot(int& i, int argc, char** argv) { // Render WOM bone hierarchy as Graphviz DOT. Mirrors // --export-quest-graph for skeleton trees: trying to read // a 50-bone tree from --info-bones output is painful; // pipe this through `dot -Tpng` for the picture. std::string base = argv[++i]; std::string outPath; if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") base = base.substr(0, base.size() - 4); if (!wowee::pipeline::WoweeModelLoader::exists(base)) { std::fprintf(stderr, "export-bones-dot: WOM not found: %s.wom\n", base.c_str()); return 1; } if (outPath.empty()) outPath = base + ".bones.dot"; auto wom = wowee::pipeline::WoweeModelLoader::load(base); std::ofstream out(outPath); if (!out) { std::fprintf(stderr, "export-bones-dot: cannot write %s\n", outPath.c_str()); return 1; } out << "digraph BoneTree {\n"; out << " // Generated by wowee_editor --export-bones-dot\n"; out << " rankdir=TB;\n"; out << " node [shape=box, style=filled, fontname=\"sans-serif\", fontsize=10];\n"; // Color: green for keybones (named anchor points), gray for // internal/blend bones. Root bones (parent=-1) get yellow border. for (size_t k = 0; k < wom.bones.size(); ++k) { const auto& b = wom.bones[k]; bool isKey = (b.keyBoneId >= 0); std::string fill = isKey ? "lightgreen" : "lightgrey"; std::string label = "[" + std::to_string(k) + "]"; if (isKey) label += "\\nkey=" + std::to_string(b.keyBoneId); out << " b" << k << " [label=\"" << label << "\", fillcolor=" << fill; if (b.parentBone == -1) out << ", penwidth=2, color=goldenrod"; out << "];\n"; } // Edges: child -> parent (parent is up). int rootCount = 0; for (size_t k = 0; k < wom.bones.size(); ++k) { int16_t p = wom.bones[k].parentBone; if (p < 0 || p >= static_cast(wom.bones.size())) { rootCount++; continue; } out << " b" << p << " -> b" << k << ";\n"; } out << "}\n"; out.close(); std::printf("Wrote %s\n", outPath.c_str()); std::printf(" %zu bones, %d root(s)\n", wom.bones.size(), rootCount); std::printf(" next: dot -Tpng %s -o bones.png\n", outPath.c_str()); return 0; } } // namespace bool handleWomInfo(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--info") == 0 && i + 1 < argc) { outRc = handleInfo(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-batches") == 0 && i + 1 < argc) { outRc = handleInfoBatches(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-textures") == 0 && i + 1 < argc) { outRc = handleInfoTextures(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-doodads") == 0 && i + 1 < argc) { outRc = handleInfoDoodads(i, argc, argv); return true; } if ((std::strcmp(argv[i], "--info-attachments") == 0 || std::strcmp(argv[i], "--info-particles") == 0 || std::strcmp(argv[i], "--info-sequences") == 0) && i + 1 < argc) { outRc = handleInfoAttachParticleSequence(i, argc, argv); return true; } if (std::strcmp(argv[i], "--info-bones") == 0 && i + 1 < argc) { outRc = handleInfoBones(i, argc, argv); return true; } if (std::strcmp(argv[i], "--export-bones-dot") == 0 && i + 1 < argc) { outRc = handleExportBonesDot(i, argc, argv); return true; } if (std::strcmp(argv[i], "--validate-wom") == 0 && i + 1 < argc) { outRc = handleValidateWom(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee