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
+180
View File
@@ -0,0 +1,180 @@
// oracle_diff —— libgr2 vs oracle 逐字段对拍(M2 门禁·主)。见 oracle/FORMAT.md、docs/steps/00-oracle.md
//
// oracle_diff <model.gr2> [<anim.gr2>|-] <oracle_dump.bin> [--t <sec>] [--mesh N]
//
// oracle_dump.bin = `oracle dump` 产出(层① 世界矩阵 + mesh N 蒙皮顶点)。
// 本工具用 libgr2 对同一 (model, anim, t) 算一遍,报 max|Δ|(矩阵)+ max‖Δ‖(顶点)。
#include "gr2/gr2.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace {
struct Reader {
const uint8_t* p;
const uint8_t* end;
bool ok = true;
uint32_t u32() {
if (p + 4 > end) { ok = false; return 0; }
uint32_t v; std::memcpy(&v, p, 4); p += 4; return v;
}
float f32() {
if (p + 4 > end) { ok = false; return 0; }
float v; std::memcpy(&v, p, 4); p += 4; return v;
}
std::string str() {
uint32_t n = u32();
if (!ok || p + n > end) { ok = false; return {}; }
std::string s((const char*)p, n); p += n; return s;
}
void skip(size_t n) { if (p + n > end) ok = false; else p += n; }
};
// 行主序 4x4 A*BColumnMatrixMultiply4x3 语义,平移 elem 12..14
void mul4x3(const float* A, const float* B, float* R) {
for (int i = 0; i < 3; ++i) {
for (int k = 0; k < 3; ++k)
R[i*4+k] = A[i*4+0]*B[0*4+k] + A[i*4+1]*B[1*4+k] + A[i*4+2]*B[2*4+k];
R[i*4+3] = 0.0f;
}
for (int k = 0; k < 3; ++k)
R[12+k] = A[12]*B[0*4+k] + A[13]*B[1*4+k] + A[14]*B[2*4+k] + B[12+k];
R[15] = 1.0f;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 4) {
std::fprintf(stderr,
"usage: oracle_diff <model.gr2> <anim.gr2|-> <dump.bin> [--t <sec>] [--mesh N]\n");
return 2;
}
const char* model_path = argv[1];
const char* anim_path = std::strcmp(argv[2], "-") == 0 ? nullptr : argv[2];
const char* bin_path = argv[3];
float t_override = -1.0f;
int mesh_index = 0;
for (int i = 4; i < argc; ++i) {
if (!std::strcmp(argv[i], "--t") && i + 1 < argc) t_override = float(std::atof(argv[++i]));
else if (!std::strcmp(argv[i], "--mesh") && i + 1 < argc) mesh_index = std::atoi(argv[++i]);
}
// ── 读 oracle dump ──────────────────────────────────────────────
FILE* f = std::fopen(bin_path, "rb");
if (!f) { std::fprintf(stderr, "cannot open %s\n", bin_path); return 1; }
std::fseek(f, 0, SEEK_END); long n = std::ftell(f); std::fseek(f, 0, SEEK_SET);
std::vector<uint8_t> buf(n > 0 ? size_t(n) : 0);
if (std::fread(buf.data(), 1, buf.size(), f) != buf.size()) { std::fclose(f); return 1; }
std::fclose(f);
Reader r{buf.data(), buf.data() + buf.size()};
if (std::memcmp(r.p, "XORC", 4) != 0) { std::fprintf(stderr, "bad magic\n"); return 1; }
r.skip(4);
uint32_t ver = r.u32(), kind = r.u32(), coord = r.u32();
char gv[16]; std::memcpy(gv, r.p, 16); r.skip(16);
std::string omodel = r.str(), oanim = r.str();
float ot = r.f32();
(void)ver; (void)coord;
std::printf("oracle dump: kind=%u granny=%.15s t=%.6f\n model=%s\n anim =%s\n",
kind, gv, ot, omodel.c_str(), oanim.c_str());
uint32_t obone_count = r.u32();
std::vector<float> omat(size_t(obone_count) * 16);
for (uint32_t b = 0; b < obone_count; ++b)
for (int k = 0; k < 16; ++k) omat[b*16+k] = r.f32();
uint32_t overt_count = r.u32();
std::vector<float> overt(size_t(overt_count) * 3);
for (uint32_t v = 0; v < overt_count; ++v)
for (int k = 0; k < 3; ++k) overt[v*3+k] = r.f32();
if (!r.ok) { std::fprintf(stderr, "dump truncated\n"); return 1; }
std::printf(" bones=%u skinned_verts=%u\n", obone_count, overt_count);
// ── libgr2 侧算一遍 ────────────────────────────────────────────
gr2::LoadError e;
auto model = gr2::File::load_path(model_path, &e);
if (!model) { std::fprintf(stderr, "libgr2 model load: %s\n", e.message.c_str()); return 1; }
if (model->file_info().skeletons.empty()) { std::fprintf(stderr, "no skeleton\n"); return 1; }
const gr2::Skeleton& sk = model->file_info().skeletons[0];
std::vector<gr2::Mat4> world, skin;
float t = t_override >= 0 ? t_override : ot;
if (anim_path) {
auto af = gr2::File::load_path(anim_path, &e);
if (!af || af->file_info().animations.empty()) {
std::fprintf(stderr, "libgr2 anim load: %s\n", e.message.c_str()); return 1;
}
gr2::sample_pose(sk, af->file_info().animations[0], t, world, skin);
} else {
gr2::bind_pose(sk, world, skin);
}
// ── 门禁 1:骨骼世界矩阵 ──────────────────────────────────────────
if (world.size() != obone_count) {
std::fprintf(stderr, "bone count mismatch: libgr2=%zu oracle=%u\n", world.size(), obone_count);
return 1;
}
double mat_max = 0.0; int mat_worst = -1;
for (uint32_t b = 0; b < obone_count; ++b) {
double d = 0.0;
for (int k = 0; k < 16; ++k) d = std::fmax(d, std::fabs((double)world[b][k] - omat[b*16+k]));
if (d > mat_max) { mat_max = d; mat_worst = int(b); }
}
std::printf("\n[gate 1] 骨骼世界矩阵 vs oracle 层①:\n");
std::printf(" max|Δ| = %.6g (worst bone %d '%s')\n", mat_max, mat_worst,
mat_worst >= 0 ? sk.bones[mat_worst].name.c_str() : "");
// ── 门禁 2:蒙皮顶点 ────────────────────────────────────────────
double vtx_max = 0.0;
bool vtx_checked = false;
if (overt_count) {
const auto& meshes = model->file_info().meshes;
if (mesh_index < (int)meshes.size()) {
const auto& me = meshes[mesh_index];
vtx_checked = true;
// libgr2 侧手算 LBS(与 engine/skinning.cpp 同公式,不引 engine 依赖)
int vtx_worst = -1;
uint32_t nv = std::min<uint32_t>(overt_count, (uint32_t)me.vertices.size());
for (uint32_t i = 0; i < nv; ++i) {
const auto& sv = me.vertices[i];
float w[4]; float sw = 0;
for (int k = 0; k < 4; ++k) { w[k] = sv.bone_weight[k] / 255.0f; sw += w[k]; }
if (sw > 1e-6f) for (int k = 0; k < 4; ++k) w[k] /= sw; else w[0] = 1.0f;
float M[16] = {0};
for (int k = 0; k < 4; ++k) {
int slot = sv.bone_index[k];
int bone = (slot >= 0 && slot < (int)me.bone_bindings.size())
? me.bone_bindings[slot] : -1;
const float* S = (bone >= 0 && bone < (int)skin.size())
? skin[bone].data() : nullptr;
if (!S) continue;
for (int j = 0; j < 16; ++j) M[j] += S[j] * w[k];
}
float o[3];
o[0] = sv.pos[0]*M[0] + sv.pos[1]*M[4] + sv.pos[2]*M[8] + M[12];
o[1] = sv.pos[0]*M[1] + sv.pos[1]*M[5] + sv.pos[2]*M[9] + M[13];
o[2] = sv.pos[0]*M[2] + sv.pos[1]*M[6] + sv.pos[2]*M[10] + M[14];
double dx = o[0]-overt[i*3+0], dy = o[1]-overt[i*3+1], dz = o[2]-overt[i*3+2];
double d = std::sqrt(dx*dx + dy*dy + dz*dz);
if (d > vtx_max) { vtx_max = d; vtx_worst = int(i); }
}
std::printf("\n[gate 2] mesh %d 蒙皮顶点 vs oracle:\n", mesh_index);
std::printf(" verts compared = %u\n", nv);
std::printf(" max‖Δ‖ = %.6g (worst vert %d)\n", vtx_max, vtx_worst);
}
}
(void)mul4x3;
// 阈值:noise_floor.json 就绪前用硬编码 1e-3(远高于实测的 ~1e-4,留浮点累积余量)
const double kMatEps = 1e-3, kVtxEps = 1e-3;
bool pass = mat_max < kMatEps && (!vtx_checked || vtx_max < kVtxEps);
std::printf("\n[verdict] granny=%.15s vs libgr2 : %s (mat %.2g < %.0e, vtx %.2g < %.0e)\n",
gv, pass ? "PASS" : "FAIL", mat_max, kMatEps,
vtx_checked ? vtx_max : 0.0, kVtxEps);
return pass ? 0 : 1;
}