Files
mtgodot-poc/tools/gr2fuzz/main.cpp
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

258 lines
10 KiB
C++

// M0 T9a/b/c —— 全量解析 + 非退化谓词 + 直方图。见 docs/steps/M0-gr2-reader.md
// gr2fuzz [assets-root] [--report out.json] [--list out.txt]
#include "gr2/gr2.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace fs = std::filesystem;
namespace {
struct Hist {
std::map<std::string, uint64_t> m;
void add(const std::string& k, uint64_t n = 1) { m[k] += n; }
};
struct Fails {
std::vector<std::string> lines;
void add(const std::string& path, const std::string& why) {
lines.push_back(path + "\t" + why);
}
};
// 非退化谓词。返回空 = 通过;否则第一条失败原因。
std::string check_predicates(const gr2::File& f) {
const auto& fi = f.file_info();
for (size_t s = 0; s < fi.skeletons.size(); ++s) {
const auto& sk = fi.skeletons[s];
if (sk.bones.empty() || sk.bones.size() > 512)
return "skeleton bone count out of [1,512]: " + std::to_string(sk.bones.size());
for (size_t i = 0; i < sk.bones.size(); ++i) {
int32_t p = sk.bones[i].parent;
if (p != -1 && (p < 0 || (size_t)p >= i))
return "bone parent not < self or -1 at " + std::to_string(i);
for (float v : sk.bones[i].local_transform)
if (!std::isfinite(v)) return "non-finite local_transform at bone " + std::to_string(i);
for (float v : sk.bones[i].inverse_world)
if (!std::isfinite(v)) return "non-finite inverse_world at bone " + std::to_string(i);
}
}
for (size_t mi = 0; mi < fi.meshes.size(); ++mi) {
const auto& me = fi.meshes[mi];
if (me.vertices.empty()) return "mesh " + std::to_string(mi) + " has 0 vertices";
for (uint32_t idx : me.indices)
if (idx >= me.vertices.size())
return "mesh " + std::to_string(mi) + " index " + std::to_string(idx) +
" >= vertex count " + std::to_string(me.vertices.size());
for (const auto& v : me.vertices)
for (float c : v.pos)
if (!std::isfinite(c)) return "mesh " + std::to_string(mi) + " non-finite position";
for (const auto& g : me.tri_groups)
if (g.tri_first < 0 || g.tri_count < 0 ||
(int64_t)(g.tri_first + g.tri_count) * 3 > (int64_t)me.indices.size())
return "mesh " + std::to_string(mi) + " tri group out of range";
if (!me.rigid) {
for (int32_t b : me.bone_bindings)
if (b < 0) return "mesh " + std::to_string(mi) + " dangling bone binding";
for (const auto& v : me.vertices) {
int wsum = 0;
for (int k = 0; k < 4; ++k) {
wsum += v.bone_weight[k];
if (!me.bone_bindings.empty() && v.bone_index[k] >= me.bone_bindings.size())
return "mesh " + std::to_string(mi) + " bone index out of range";
}
// u8 权重和:3 显式 + 1 隐式的情况下允许补齐,故用宽松带
if (wsum < 200 || wsum > 262) // 255 ± ~10%(含隐式第 4 权重补齐余量)
return "mesh " + std::to_string(mi) + " weight sum " + std::to_string(wsum) +
" out of [200,262]";
}
}
}
return {};
}
void dump_report(const fs::path& out, uint64_t total, uint64_t ok, uint64_t crashed,
uint64_t pred_fail, Hist& ver, Hist& comp, Hist& vkind, Hist& curve,
Hist& weights, Hist& ssuse, Hist& selfchk,
const Fails& crashes, const Fails& preds) {
FILE* fp = std::fopen(out.string().c_str(), "wb");
if (!fp) { std::fprintf(stderr, "cannot write %s\n", out.string().c_str()); return; }
auto obj = [&](const char* name, Hist& h, bool last) {
std::fprintf(fp, " \"%s\": {", name);
bool first = true;
for (auto& [k, v] : h.m) {
std::fprintf(fp, "%s\n \"%s\": %llu", first ? "" : ",", k.c_str(),
(unsigned long long)v);
first = false;
}
std::fprintf(fp, "\n }%s\n", last ? "" : ",");
};
auto arr = [&](const char* name, const Fails& fl, bool last) {
std::fprintf(fp, " \"%s\": [", name);
for (size_t i = 0; i < fl.lines.size(); ++i) {
std::string s = fl.lines[i];
std::string esc;
for (char c : s) { if (c == '\\' || c == '"') esc += '\\'; esc += (c == '\t' ? ' ' : c); }
std::fprintf(fp, "%s\n \"%s\"", i ? "," : "", esc.c_str());
}
std::fprintf(fp, "\n ]%s\n", last ? "" : ",");
};
std::fprintf(fp, "{\n");
std::fprintf(fp, " \"total\": %llu,\n \"ok\": %llu,\n \"crashed\": %llu,\n \"predicate_fail\": %llu,\n",
(unsigned long long)total, (unsigned long long)ok,
(unsigned long long)crashed, (unsigned long long)pred_fail);
obj("format_version", ver, false);
obj("section_compression", comp, false);
obj("vertex_kind", vkind, false);
obj("bone_weight_slots", weights, false);
obj("curve_subtype", curve, false);
obj("scaleshear_usage", ssuse, false);
obj("bind_pose_self_check", selfchk, false);
arr("crashes", crashes, false);
arr("predicate_failures", preds, true);
std::fprintf(fp, "}\n");
std::fclose(fp);
}
} // namespace
#ifndef XRENDER_ASSET_ROOT
#define XRENDER_ASSET_ROOT "." // 无编译期默认:从命令行传资产根目录
#endif
int main(int argc, char** argv) {
std::string root = XRENDER_ASSET_ROOT;
fs::path report = "fuzz-report.json";
fs::path listout;
for (int i = 1; i < argc; ++i) {
if (!std::strcmp(argv[i], "--report") && i + 1 < argc) report = argv[++i];
else if (!std::strcmp(argv[i], "--list") && i + 1 < argc) listout = argv[++i];
else root = argv[i];
}
if (!fs::exists(root)) {
std::fprintf(stderr, "asset root not found: %s\n", root.c_str());
return 2;
}
uint64_t total = 0, ok = 0, crashed = 0, pred_fail = 0;
Hist ver, comp, vkind, curve, weights, ssuse, selfchk;
Fails crashes, preds;
std::vector<std::string> good_files;
static const char* kK[] = {"PNT332", "PNT3322", "PNT332_Skinned", "PNT3322_Skinned", "Unknown"};
for (auto& e : fs::recursive_directory_iterator(
root, fs::directory_options::skip_permission_denied)) {
if (!e.is_regular_file()) continue;
auto p = e.path();
std::string ext = p.extension().string();
for (auto& c : ext) c = (char)std::tolower((unsigned char)c);
if (ext != ".gr2") continue;
++total;
gr2::LoadError err;
std::optional<gr2::File> f;
try {
f = gr2::File::load_path(p.string(), &err);
} catch (const std::exception& ex) {
++crashed; crashes.add(p.string(), std::string("exception: ") + ex.what());
continue;
} catch (...) {
++crashed; crashes.add(p.string(), "exception: unknown");
continue;
}
if (!f) {
++crashed; crashes.add(p.string(), "load[" + err.stage + "]: " + err.message);
continue;
}
ver.add(std::to_string(f->format_version()));
for (const auto& s : f->sections()) {
comp.add(gr2::compression_name(s.compression));
if (s.expanded_size && s.decompress_status != 0) {
++crashed;
crashes.add(p.string(), "decompress status " + std::to_string(s.decompress_status));
}
}
const auto& fi = f->file_info();
// bind pose 自洽(T5):对每个 skeleton 取最好(最小)偏差,按量级分桶
for (size_t s = 0; s < fi.skeletons.size(); ++s) {
if (fi.skeletons[s].bones.size() < 2) continue; // 单骨挂点跳过
double dev = f->bind_pose_self_check(int(s));
const char* bucket = dev < 1e-4 ? "<1e-4"
: dev < 1e-3 ? "<1e-3"
: dev < 1e-2 ? "<1e-2"
: dev < 1e-1 ? "<1e-1" : ">=1e-1";
selfchk.add(bucket);
if (dev >= 1e-1 && getenv("GR2_SELFCHK"))
std::fprintf(stderr, "SELFCHK %.4f skel%zu bones=%zu %s\n",
dev, s, fi.skeletons[s].bones.size(), p.string().c_str());
}
for (const auto& me : fi.meshes) {
vkind.add(kK[int(me.kind)]);
if (!me.rigid) {
weights.add(std::to_string(me.source_bone_weight_slots));
}
}
for (const auto& sk : fi.skeletons) {
uint64_t ss = 0;
for (const auto& b : sk.bones) if (b.lt_flags & 0x4) ++ss;
ssuse.add(ss ? "has_scaleshear" : "no_scaleshear", 1);
ssuse.add("scaleshear_bones", ss);
}
for (const auto& an : fi.animations)
for (const auto& t : an.tracks) {
curve.add("pos:" + t.pos_type);
curve.add("rot:" + t.rot_type);
curve.add("scale:" + t.scale_type);
}
std::string why;
try {
why = check_predicates(*f);
} catch (...) {
why = "predicate check threw";
}
if (!why.empty()) { ++pred_fail; preds.add(p.string(), why); continue; }
++ok;
good_files.push_back(p.string());
}
dump_report(report, total, ok, crashed, pred_fail, ver, comp, vkind, curve, weights, ssuse,
selfchk, crashes, preds);
if (!listout.empty()) {
FILE* fp = std::fopen(listout.string().c_str(), "wb");
if (fp) { for (auto& s : good_files) std::fprintf(fp, "%s\n", s.c_str()); std::fclose(fp); }
}
std::printf("gr2fuzz: total=%llu ok=%llu crashed=%llu predicate_fail=%llu\n",
(unsigned long long)total, (unsigned long long)ok,
(unsigned long long)crashed, (unsigned long long)pred_fail);
std::printf(" report: %s\n", report.string().c_str());
if (!crashes.lines.empty()) {
std::printf(" first crashes:\n");
for (size_t i = 0; i < crashes.lines.size() && i < 20; ++i)
std::printf(" %s\n", crashes.lines[i].c_str());
}
if (!preds.lines.empty()) {
std::printf(" first predicate failures:\n");
for (size_t i = 0; i < preds.lines.size() && i < 20; ++i)
std::printf(" %s\n", preds.lines[i].c_str());
}
return (crashed == 0 && pred_fail == 0) ? 0 : 1;
}