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
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// 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] [--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<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;
|
||||
}
|
||||
Reference in New Issue
Block a user