diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 8cec89c0..4d1fb596 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -701,6 +701,8 @@ static void printUsage(const char* argv0) { std::printf(" List M2 animation sequences (id, duration, flags)\n"); std::printf(" --info-bones [--json]\n"); std::printf(" List M2 bones with parent tree, key-bone IDs, pivot offsets\n"); + std::printf(" --export-bones-dot [out.dot]\n"); + std::printf(" Render WOM bone hierarchy as Graphviz DOT (pipe to `dot -Tpng -o bones.png`)\n"); std::printf(" --list-zone-textures [--json]\n"); std::printf(" Aggregate texture refs across all WOM models in a zone (deduped)\n"); std::printf(" --info-wob [--json]\n"); @@ -820,7 +822,7 @@ int main(int argc, char* argv[]) { static const char* kArgRequired[] = { "--data", "--info", "--info-batches", "--info-textures", "--info-doodads", "--info-attachments", "--info-particles", "--info-sequences", - "--info-bones", "--list-zone-textures", + "--info-bones", "--export-bones-dot", "--list-zone-textures", "--info-wob", "--info-woc", "--info-wot", "--info-creatures", "--info-objects", "--info-quests", "--info-extract", "--info-extract-tree", "--info-extract-budget", @@ -1526,6 +1528,63 @@ int main(int argc, char* argv[]) { b.pivot.x, b.pivot.y, b.pivot.z); } return 0; + } else if (std::strcmp(argv[i], "--export-bones-dot") == 0 && i + 1 < argc) { + // 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; } else if (std::strcmp(argv[i], "--list-zone-textures") == 0 && i + 1 < argc) { // Aggregate texture references across every WOM model in a // zone directory. Companion to --list-zone-deps (which lists