// M0 T8 —— gr2dump CLI。见 docs/steps/M0-gr2-reader.md // gr2dump [--sections] [--gltf out.glb] #include "gr2/gr2.h" #include #include #include #include #include // 从展开后的 section 里找可打印 ASCII 串(>= minlen)。 // 用途:Metin2 gr2 都内嵌 "d:\ymir work\...\xxx.gr2" 之类的路径 —— // 解压正确的话这些串会原样出现,是不依赖 oracle 的内容正确性锚。 static int dump_strings(const gr2::File& f, size_t minlen) { int hits = 0; for (size_t i = 0; i < f.sections().size(); ++i) { auto b = f.section_bytes(i); std::string cur; for (size_t k = 0; k <= b.size(); ++k) { unsigned char c = (k < b.size()) ? b[k] : 0; if (c >= 0x20 && c < 0x7f) { cur += char(c); continue; } if (cur.size() >= minlen) { std::printf(" [sec %zu] %s\n", i, cur.c_str()); ++hits; } cur.clear(); } } return hits; } int main(int argc, char** argv) { if (argc < 2) { std::fprintf(stderr, "usage: gr2dump [--sections] [--strings] [--gltf out.glb]\n"); return 2; } const std::string path = argv[1]; bool want_sections = false, want_strings = false, want_members = false; std::string gltf_out; for (int i = 2; i < argc; ++i) { if (!std::strcmp(argv[i], "--sections")) want_sections = true; else if (!std::strcmp(argv[i], "--strings")) want_strings = true; else if (!std::strcmp(argv[i], "--members")) want_members = true; else if (!std::strcmp(argv[i], "--gltf") && i + 1 < argc) gltf_out = argv[++i]; } gr2::LoadError err; auto file = gr2::File::load_path(path, &err); if (!file) { std::fprintf(stderr, "load failed [%s]: %s\n", err.stage.c_str(), err.message.c_str()); return 1; } std::printf("magic : %s\n", gr2::magic_name(file->magic())); std::printf("format_version : %u\n", file->format_version()); std::printf("total_size : %u\n", file->total_size()); std::printf("sections : %zu\n", file->sections().size()); if (want_sections) { std::printf("\n idx compression data_size expanded ratio ptr_fixups decomp\n"); std::printf(" --- ----------- --------- -------- ----- ---------- ------\n"); int idx = 0, ok = 0, nonempty = 0; for (const auto& s : file->sections()) { double ratio = s.expanded_size ? double(s.data_size) / s.expanded_size : 0.0; const char* st = s.expanded_size == 0 ? "empty" : s.decompress_status == 0 ? "OK" : "FAIL"; if (s.expanded_size) { ++nonempty; if (s.decompress_status == 0) ++ok; } std::printf(" %3d %-11s %9u %8u %4.2f %10u %s\n", idx++, gr2::compression_name(s.compression), s.data_size, s.expanded_size, ratio, s.pointer_fixup_count, st); } std::printf(" → decompress: %d/%d non-empty sections OK\n", ok, nonempty); } if (want_strings) { std::printf("\nembedded strings (>=6 chars, from decompressed sections):\n"); int n = dump_strings(*file, 6); std::printf(" → %d strings\n", n); } if (want_members) { static const char* kMT[] = { "End","Inline","Reference","RefToArray","ArrayOfRefs","VariantRef", "Removed","RefToVariantArray","String","Transform","Real32","Int8", "UInt8","BinormInt8","NormUInt8","Int16","UInt16","BinormInt16", "NormUInt16","Int32","UInt32","Real16","EmptyRef"}; std::printf("\nroot object type members (T3 typetree walk):\n"); auto ms = gr2::dump_root_members(*file); for (const auto& m : ms) { const char* tn = (m.member_type >= 0 && m.member_type < 23) ? kMT[m.member_type] : "?"; std::printf(" %-24s %-18s array_width=%d\n", m.name.c_str(), tn, m.array_width); } std::printf(" → %zu members\n", ms.size()); } // ── T4/T5/T6:FileInfo 摘要 ────────────────────────────────────── const auto& fi = file->file_info(); std::printf("\nFileInfo:\n"); std::printf(" from_file_name : %s\n", fi.from_file_name.c_str()); std::printf(" skeletons=%zu meshes=%zu animations=%zu materials=%u textures=%u models=%u\n", fi.skeletons.size(), fi.meshes.size(), fi.animations.size(), fi.material_count, fi.texture_count, fi.model_count); for (size_t i = 0; i < fi.materials.size(); ++i) std::printf(" material %zu: %-24s tex=%s\n", i, fi.materials[i].name.c_str(), fi.materials[i].diffuse_texture.c_str()); for (size_t si = 0; si < fi.skeletons.size(); ++si) { const auto& sk = fi.skeletons[si]; int nonI_ori = 0, nonI_ss = 0; for (const auto& b : sk.bones) { if (b.lt_flags & 0x2) ++nonI_ori; if (b.lt_flags & 0x4) ++nonI_ss; } std::printf("\n skeleton %zu: %zu bones (non-identity: orientation=%d scaleshear=%d)\n", si, sk.bones.size(), nonI_ori, nonI_ss); for (size_t i = 0; i < sk.bones.size(); ++i) { const auto& b = sk.bones[i]; int depth = 0; for (int p = b.parent; p >= 0 && depth < 64; ) { ++depth; p = sk.bones[p].parent; } std::printf(" %*s[%zu] %s (parent=%d)\n", depth * 2, "", i, b.name.c_str(), b.parent); } } for (size_t mi = 0; mi < fi.meshes.size(); ++mi) { const auto& me = fi.meshes[mi]; static const char* kK[] = {"PNT332","PNT3322","PNT332_Skinned","PNT3322_Skinned","Unknown"}; std::printf("\n mesh %zu: %s\n", mi, me.name.c_str()); std::printf(" kind=%s rigid=%d vertices=%zu indices=%zu tri_groups=%zu bone_bindings=%zu\n", kK[int(me.kind)], me.rigid, me.vertices.size(), me.indices.size(), me.tri_groups.size(), me.bone_bindings.size()); for (size_t g = 0; g < me.tri_groups.size(); ++g) { int mix = me.tri_groups[g].material_index; const char* tn = (mix >= 0 && mix < (int)me.material_textures.size()) ? me.material_textures[mix].c_str() : ""; std::printf(" group %zu: material=%d tri_first=%d tri_count=%d tex=%s\n", g, mix, me.tri_groups[g].tri_first, me.tri_groups[g].tri_count, tn); } for (size_t b = 0; b < me.material_textures.size(); ++b) std::printf(" binding %zu -> %s\n", b, me.material_textures[b].c_str()); int dangling = 0; for (int32_t b : me.bone_bindings) if (b < 0) ++dangling; if (!me.bone_bindings.empty()) std::printf(" dangling bone bindings: %d\n", dangling); } for (size_t ai = 0; ai < fi.animations.size(); ++ai) { const auto& an = fi.animations[ai]; std::printf("\n animation %zu: %s duration=%.3f tracks=%zu\n", ai, an.name.c_str(), an.duration, an.tracks.size()); std::map hist; int mapped = 0; for (const auto& t : an.tracks) { hist["pos:" + t.pos_type]++; hist["rot:" + t.rot_type]++; hist["scale:" + t.scale_type]++; if (t.bone_index >= 0) ++mapped; } std::printf(" curve subtype histogram:\n"); for (const auto& [k, v] : hist) std::printf(" %-28s %d\n", k.c_str(), v); std::printf(" tracks mapped to skeleton bones: %d/%zu\n", mapped, an.tracks.size()); // T7b:在几个采样点求值,报非有限数 + root track 局部平移 int pos_c = 0, rot_c = 0, ss_c = 0; for (const auto& t : an.tracks) { if (!t.position.empty()) ++pos_c; if (!t.orientation.empty()) ++rot_c; if (!t.scale_shear.empty()) ++ss_c; } std::printf(" curves decoded (T7b): pos=%d rot=%d scaleshear=%d\n", pos_c, rot_c, ss_c); for (float frac : {0.0f, 0.5f, 1.0f}) { float tt = an.duration * frac; std::vector loc; an.sample_local(tt, loc); int bad = 0; for (const auto& m : loc) for (float x : m) if (!std::isfinite(x)) ++bad; std::printf(" sample t=%7.3f non-finite=%d", tt, bad); if (!loc.empty()) std::printf(" track0 '%s' pos=(%.2f,%.2f,%.2f)", an.tracks[0].bone_name.c_str(), loc[0][12], loc[0][13], loc[0][14]); std::printf("\n"); } } // ── T5:bind pose 自洽 ─────────────────────────────────────────── if (!fi.skeletons.empty()) { double dev = file->bind_pose_self_check(0); std::printf("\nbind pose self-check: %s (max|delta|=%.3g, threshold=1e-3)\n", dev < 1e-3 ? "PASS" : "FAIL", dev); } else { std::printf("\nbind pose self-check: n/a (no skeleton in this file)\n"); } if (!gltf_out.empty()) std::fprintf(stderr, "--gltf not implemented yet (M0 T8)\n"); return 0; }