// 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] [--members] [--transforms] [--gltf out.glb]\n"); return 2; } const std::string path = argv[1]; bool want_sections = false, want_strings = false, want_members = false, want_transforms = 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], "--transforms")) want_transforms = 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); if (want_transforms) { std::printf(" local.t=(%.3f,%.3f,%.3f) inverse_world.t=(%.3f,%.3f,%.3f)\n", b.local_transform[12], b.local_transform[13], b.local_transform[14], b.inverse_world[12], b.inverse_world[13], b.inverse_world[14]); } } if (want_transforms) std::printf(" initial_placement.t=(%.3f,%.3f,%.3f)\n", sk.initial_placement[12], sk.initial_placement[13], sk.initial_placement[14]); } 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); int g_up = 0, g_down = 0, g_side = 0; for (int t = 0; t < me.tri_groups[g].tri_count; ++t) { uint32_t idx = (me.tri_groups[g].tri_first + t) * 3; if (idx + 2 < me.indices.size()) { uint32_t i0 = me.indices[idx], i1 = me.indices[idx+1], i2 = me.indices[idx+2]; const auto& v0 = me.vertices[i0]; const auto& v1 = me.vertices[i1]; const auto& v2 = me.vertices[i2]; float e1[3] = {v1.pos[0]-v0.pos[0], v1.pos[1]-v0.pos[1], v1.pos[2]-v0.pos[2]}; float e2[3] = {v2.pos[0]-v0.pos[0], v2.pos[1]-v0.pos[1], v2.pos[2]-v0.pos[2]}; float nz = e1[0]*e2[1] - e1[1]*e2[0]; if (nz > 1e-3f) ++g_up; else if (nz < -1e-3f) ++g_down; else ++g_side; } } std::printf(" tri face normal: +Z(up)=%d -Z(down)=%d sideways=%d\n", g_up, g_down, g_side); } 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); if (want_transforms && !me.vertices.empty()) { float lo[3] = {me.vertices[0].pos[0], me.vertices[0].pos[1], me.vertices[0].pos[2]}; float hi[3] = {lo[0], lo[1], lo[2]}; for (const auto& v : me.vertices) for (int a = 0; a < 3; ++a) { lo[a] = std::min(lo[a], v.pos[a]); hi[a] = std::max(hi[a], v.pos[a]); } std::printf(" aabb min=(%.3f,%.3f,%.3f) max=(%.3f,%.3f,%.3f)\n", lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]); int normal_agree = 0, normal_disagree = 0; for (size_t t = 0; t + 2 < me.indices.size(); t += 3) { uint32_t i0 = me.indices[t + 0]; uint32_t i1 = me.indices[t + 1]; uint32_t i2 = me.indices[t + 2]; if (i0 < me.vertices.size() && i1 < me.vertices.size() && i2 < me.vertices.size()) { const auto& v0 = me.vertices[i0]; const auto& v1 = me.vertices[i1]; const auto& v2 = me.vertices[i2]; float e1[3] = {v1.pos[0] - v0.pos[0], v1.pos[1] - v0.pos[1], v1.pos[2] - v0.pos[2]}; float e2[3] = {v2.pos[0] - v0.pos[0], v2.pos[1] - v0.pos[1], v2.pos[2] - v0.pos[2]}; float nx = e1[1]*e2[2] - e1[2]*e2[1]; float ny = e1[2]*e2[0] - e1[0]*e2[2]; float nz = e1[0]*e2[1] - e1[1]*e2[0]; float dot = nx * v0.normal[0] + ny * v0.normal[1] + nz * v0.normal[2]; if (dot > 0.0f) ++normal_agree; else if (dot < 0.0f) ++normal_disagree; } } std::printf(" winding vs normal: agree=%d disagree=%d\n", normal_agree, normal_disagree); float avg_n[3] = {0,0,0}; for (const auto& v : me.vertices) { avg_n[0] += v.normal[0]; avg_n[1] += v.normal[1]; avg_n[2] += v.normal[2]; } if (!me.vertices.empty()) { float inv = 1.0f / me.vertices.size(); std::printf(" avg normal: (%.3f, %.3f, %.3f)\n", avg_n[0]*inv, avg_n[1]*inv, avg_n[2]*inv); } int z_up = 0, z_down = 0, xy_side = 0; for (const auto& v : me.vertices) { if (v.normal[2] > 0.5f) ++z_up; else if (v.normal[2] < -0.5f) ++z_down; else ++xy_side; } std::printf(" vertex normal dir: +Z(up)=%d -Z(down)=%d sideways=%d\n", z_up, z_down, xy_side); if (me.indices.size() >= 6) { for (int tr = 0; tr < 2; ++tr) { uint32_t i0 = me.indices[tr*3+0], i1 = me.indices[tr*3+1], i2 = me.indices[tr*3+2]; const auto &v0 = me.vertices[i0], &v1 = me.vertices[i1], &v2 = me.vertices[i2]; std::printf(" tri%d: v0=(%.1f,%.1f,%.1f) n=(%.2f,%.2f,%.2f)\n" " v1=(%.1f,%.1f,%.1f) n=(%.2f,%.2f,%.2f)\n" " v2=(%.1f,%.1f,%.1f) n=(%.2f,%.2f,%.2f)\n", tr, v0.pos[0], v0.pos[1], v0.pos[2], v0.normal[0], v0.normal[1], v0.normal[2], v1.pos[0], v1.pos[1], v1.pos[2], v1.normal[0], v1.normal[1], v1.normal[2], v2.pos[0], v2.pos[1], v2.pos[2], v2.normal[0], v2.normal[1], v2.normal[2]); } } } } 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; }