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:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+195
View File
@@ -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/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);
}
}
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");
}
}
// ── 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;
}