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,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*B(ColumnMatrixMultiply4x3 语义,平移 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;
|
||||
}
|
||||
Reference in New Issue
Block a user