- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
276 lines
14 KiB
C++
276 lines
14 KiB
C++
// 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/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<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");
|
||
}
|
||
}
|
||
|
||
// ── 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;
|
||
}
|