Files
mtgodot-poc/tools/gr2dump/main.cpp
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

276 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// M0 T8 —— gr2dump CLI。见 docs/steps/M0-gr2-reader.md
// gr2dump <file.gr2> [--sections] [--gltf out.glb]
#include "gr2/gr2.h"
#include <cstdio>
#include <cstring>
#include <cmath>
#include <map>
#include <string>
// 从展开后的 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 <file.gr2> [--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/T6FileInfo 摘要 ──────────────────────────────────────
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<std::string, int> 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<gr2::Mat4> 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");
}
}
// ── T5bind 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;
}