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
+300
View File
@@ -0,0 +1,300 @@
// M0 T6 —— 顶点 / 索引 / 三角组 / BoneBindings。见 docs/steps/M0-gr2-reader.md
// 规格:granny_mesh.cpp (MeshType/BoneBindingType) / granny_vertex_data.cpp (VertexDataType
// + 各顶点格式) / granny_tri_topology.cpp (TriTopologyType/TriMaterialGroupType)
// 顶点格式是自描述的(Vertices 是 ReferenceToVariantArray,带 data_type_definition*),
// 按成员名(Position/Normal/TextureCoordinates0/1/BoneWeights/BoneIndices)取分量,不硬编码布局。
#include "internal.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#define MDBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[mesh] " __VA_ARGS__); } while (0)
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
namespace {
// member_type → (元素是浮点?, 元素字节数, 是否归一化整型)
struct Elem { bool is_float; int bytes; bool normalized; bool is_signed; };
Elem elem_of(int mt) {
switch (mt) {
case detail::MT_Real32: return {true, 4, false, true};
case detail::MT_Real16: return {false, 2, false, true}; // 半精度,少见,按需扩展
case detail::MT_Int8: return {false, 1, false, true};
case detail::MT_UInt8: return {false, 1, false, false};
case detail::MT_BinormInt8: return {false, 1, true, true};
case detail::MT_NormUInt8: return {false, 1, true, false};
case detail::MT_Int16: return {false, 2, false, true};
case detail::MT_UInt16: return {false, 2, false, false};
case detail::MT_BinormInt16: return {false, 2, true, true};
case detail::MT_NormUInt16: return {false, 2, true, false};
case detail::MT_Int32: return {false, 4, false, true};
case detail::MT_UInt32: return {false, 4, false, false};
default: return {false, 0, false, false};
}
}
float read_scalar(const uint8_t* p, const Elem& e) {
switch (e.bytes) {
case 4:
if (e.is_float) { float v; std::memcpy(&v, p, 4); return v; }
if (e.is_signed) { int32_t v; std::memcpy(&v, p, 4); return float(v); }
{ uint32_t v; std::memcpy(&v, p, 4); return float(v); }
case 2: {
if (e.is_signed) { int16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 32767.0f : float(v); }
uint16_t v; std::memcpy(&v, p, 2); return e.normalized ? v / 65535.0f : float(v);
}
case 1: {
if (e.is_signed) { int8_t v = (int8_t)p[0]; return e.normalized ? v / 127.0f : float(v); }
return e.normalized ? p[0] / 255.0f : float(p[0]);
}
default: return 0.0f;
}
}
} // namespace
// mesh: {String Name; Reference PrimaryVertexData; RefToArray MorphTargets;
// Reference PrimaryTopology; RefToArray MaterialBindings; RefToArray BoneBindings; ...}
bool File::Impl::extract_mesh(Ref mesh_obj, Ref mesh_type,
const std::vector<Skeleton>& skels, Mesh& out) {
if (!mesh_obj.valid() || !mesh_type.valid()) return false;
const auto& mms = parse_type(mesh_type);
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[mesh] mesh_type=(%d,%u) members=%zu:\n",
mesh_type.section, mesh_type.offset, mms.size());
for (auto& m : mms)
fprintf(stderr, "[mesh] %-20s type=%d aw=%d off=%u size=%u reftype=(%d,%u)\n",
m.name.c_str(), m.type, m.array_width, m.offset, m.size,
m.ref_type.section, m.ref_type.offset);
}
if (const TypeMember* mName = find_member(mms, "Name"))
out.name = rd_str(member_slot(mesh_obj, *mName));
// ── 顶点 ──────────────────────────────────────────────────────────
const TypeMember* mVD = find_member(mms, "PrimaryVertexData");
if (!mVD) { MDBG("no PrimaryVertexData member\n"); return false; }
MDBG("PrimaryVertexData m.offset=%u ref_type=(%d,%u)\n",
mVD->offset, mVD->ref_type.section, mVD->ref_type.offset);
Ref vd = read_reference(mesh_obj, *mVD);
if (!vd.valid()) { MDBG("PrimaryVertexData ref -> NULL (slot %d,%u)\n",
member_slot(mesh_obj, *mVD).section, member_slot(mesh_obj, *mVD).offset); return false; }
const auto& vdms = parse_type(mVD->ref_type);
const TypeMember* mVerts = find_member(vdms, "Vertices");
if (!mVerts) { MDBG("no Vertices member (vdms=%zu)\n", vdms.size()); return false; }
// ReferenceToVariantArray on-disk (32-bit): {def* Type; int32 Count; void* Ptr}
// (granny_data_type_definition.cpp: Ptr32 + int32 + Ptr32)
Ref vslot = member_slot(vd, *mVerts);
Ref vtype = follow({vslot.section, vslot.offset + 0});
int32_t vcount = rd_i32({vslot.section, vslot.offset + 4});
Ref vdata = follow({vslot.section, vslot.offset + 8});
MDBG("Vertices count=%d vtype=(%d,%u) vdata=(%d,%u)\n",
vcount, vtype.section, vtype.offset, vdata.section, vdata.offset);
if (vcount <= 0 || vcount > (1 << 24) || !vtype.valid() || !vdata.valid()) return false;
const auto& vfmt = parse_type(vtype);
uint32_t stride = 0;
for (const auto& m : vfmt) stride += m.size;
MDBG("vfmt members=%zu stride=%u\n", vfmt.size(), stride);
if (stride == 0) return false;
struct Comp { const TypeMember* m; Elem e; };
auto comp = [&](const char* n) -> Comp {
const TypeMember* m = find_member(vfmt, n);
if (!m) return {nullptr, {}};
return {m, elem_of(m->type)};
};
Comp cPos = comp("Position");
Comp cNrm = comp("Normal");
Comp cUv0 = comp("TextureCoordinates0");
Comp cUv1 = comp("TextureCoordinates1");
Comp cBW = comp("BoneWeights");
Comp cBI = comp("BoneIndices");
const bool skinned = (cBW.m && cBI.m);
out.rigid = !skinned;
if (skinned) {
out.source_bone_weight_slots = uint32_t(cBW.m->array_width > 0 ? cBW.m->array_width : 1);
out.source_bone_index_slots = (cBI.e.bytes == 4 && cBI.m->array_width <= 1)
? 4u
: uint32_t(cBI.m->array_width > 0 ? cBI.m->array_width : 1);
}
if (skinned && cUv1.m) out.kind = VertexKind::PNT3322_Skinned;
else if (skinned) out.kind = VertexKind::PNT332_Skinned;
else if (cUv1.m) out.kind = VertexKind::PNT3322;
else if (cPos.m && cNrm.m) out.kind = VertexKind::PNT332;
else out.kind = VertexKind::Unknown;
if ((uint64_t)vdata.offset + (uint64_t)vcount * stride > sections[vdata.section].data.size())
return false;
out.vertices.resize(vcount);
for (int32_t i = 0; i < vcount; ++i) {
const uint8_t* base = sections[vdata.section].data.data() + vdata.offset + size_t(i) * stride;
Vertex& v = out.vertices[i];
auto rd = [&](const Comp& c, float* dst, int n) {
if (!c.m || !c.e.bytes) return;
int aw = c.m->array_width <= 0 ? 1 : c.m->array_width;
n = n < aw ? n : aw;
for (int k = 0; k < n; ++k)
dst[k] = read_scalar(base + c.m->offset + size_t(k) * c.e.bytes, c.e);
};
rd(cPos, v.pos, 3);
rd(cNrm, v.normal, 3);
rd(cUv0, v.uv0, 2);
rd(cUv1, v.uv1, 2);
if (skinned) {
float w[4] = {0,0,0,0}, bi[4] = {0,0,0,0};
rd(cBW, w, 4);
// BoneIndices 可能是 UInt8[4] 或 UInt32[N]PWN* 系列是单 uint32 packed 4×u8
if (cBI.e.bytes == 1) {
rd(cBI, bi, 4);
} else if (cBI.e.bytes == 4 && cBI.e.is_float == false) {
uint32_t packed;
std::memcpy(&packed, base + cBI.m->offset, 4);
for (int k = 0; k < 4; ++k) bi[k] = float((packed >> (k * 8)) & 0xff);
}
for (int k = 0; k < 4; ++k) {
v.bone_index[k] = uint8_t(std::lround(bi[k]));
float wn = cBW.e.normalized ? w[k] : w[k]; // 已在 read_scalar 归一
v.bone_weight[k] = uint8_t(std::lround(std::fmin(std::fmax(wn, 0.0f), 1.0f) * 255.0f));
}
}
}
// ── 索引 + 三角组 ────────────────────────────────────────────────
const TypeMember* mTopo = find_member(mms, "PrimaryTopology");
if (mTopo) {
Ref tt = read_reference(mesh_obj, *mTopo);
if (tt.valid()) {
const auto& tms = parse_type(mTopo->ref_type);
if (const TypeMember* mG = find_member(tms, "Groups")) {
Ref gb; uint32_t gc = 0, gs = 0;
if (read_ref_to_array(tt, *mG, gb, gc, gs) && gc) {
const auto& gmT = parse_type(mG->ref_type);
const TypeMember* mMI = find_member(gmT, "MaterialIndex");
const TypeMember* mTF = find_member(gmT, "TriFirst");
const TypeMember* mTC = find_member(gmT, "TriCount");
for (uint32_t i = 0; i < gc; ++i) {
Ref g = {gb.section, gb.offset + i * gs};
TriGroup tg{};
if (mMI) tg.material_index = rd_i32(member_slot(g, *mMI));
if (mTF) tg.tri_first = rd_i32(member_slot(g, *mTF));
if (mTC) tg.tri_count = rd_i32(member_slot(g, *mTC));
out.tri_groups.push_back(tg);
}
}
}
auto load_idx = [&](const char* name, int elem_bytes) -> bool {
const TypeMember* mI = find_member(tms, name);
if (!mI) return false;
Ref ib; uint32_t ic = 0, is = 0;
if (!read_ref_to_array(tt, *mI, ib, ic, is) || ic == 0) return false;
const uint8_t* p = at(ib, size_t(ic) * elem_bytes);
if (!p) return false;
out.indices.resize(ic);
for (uint32_t i = 0; i < ic; ++i) {
if (elem_bytes == 2) { uint16_t v; std::memcpy(&v, p + i*2, 2); out.indices[i] = v; }
else { uint32_t v; std::memcpy(&v, p + i*4, 4); out.indices[i] = v; }
}
return true;
};
if (!load_idx("Indices", 4)) load_idx("Indices16", 2);
}
}
// ── MaterialBindings:与 tri_group.material_index 平行的贴图名表 ───
// granny_mesh.MaterialBindings = RefToArray of {Reference Material}。
if (const TypeMember* mMB = find_member(mms, "MaterialBindings")) {
Ref mb; uint32_t mc = 0, ms = 0;
if (read_ref_to_array(mesh_obj, *mMB, mb, mc, ms) && mc) {
const auto& bT = parse_type(mMB->ref_type);
const TypeMember* mMat = find_member(bT, "Material");
out.material_textures.reserve(mc);
for (uint32_t i = 0; i < mc; ++i) {
Ref e = {mb.section, mb.offset + i * ms};
std::string tn;
if (mMat) {
Ref mat = read_reference(e, *mMat);
tn = material_texture_name(mat, mMat->ref_type, 0);
}
out.material_textures.push_back(std::move(tn));
}
}
}
// ── BoneBindingsmesh 局部骨骼槽 → skeleton 骨骼索引 ─────────────
// 文件可能带多个 skeleton(本体 + 武器挂点等)。对每个 skeleton 试解析,
// 取"悬空最少"的那个。
if (const TypeMember* mBB = find_member(mms, "BoneBindings")) {
Ref bb; uint32_t bc = 0, bs = 0;
if (read_ref_to_array(mesh_obj, *mBB, bb, bc, bs) && bc) {
const auto& bbT = parse_type(mBB->ref_type);
const TypeMember* mBN = find_member(bbT, "BoneName");
std::vector<std::string> names(bc);
for (uint32_t i = 0; i < bc; ++i) {
Ref e = {bb.section, bb.offset + i * bs};
names[i] = mBN ? rd_str(member_slot(e, *mBN)) : std::string();
}
std::vector<int32_t> best(bc, -1);
int best_dangling = int(bc) + 1;
for (const auto& sk : skels) {
std::vector<int32_t> cur(bc, -1);
int dangling = 0;
for (uint32_t i = 0; i < bc; ++i) {
for (size_t k = 0; k < sk.bones.size(); ++k)
if (sk.bones[k].name == names[i]) { cur[i] = int32_t(k); break; }
if (cur[i] < 0) ++dangling;
}
if (dangling < best_dangling) { best_dangling = dangling; best = cur; }
if (dangling == 0) break;
}
out.bone_bindings = std::move(best);
}
}
return true;
}
// granny_material {String Name; int MapCount; material_map* Maps; texture* Texture; ...}
// granny_material_map {String Usage; material* Material}
// granny_texture {String FromFileName; ...}
std::string File::Impl::material_texture_name(Ref mat_obj, Ref mat_type, int depth) {
if (!mat_obj.valid() || !mat_type.valid() || depth > 8) return {};
const auto& mms = parse_type(mat_type);
// 1) 直接挂 texture
if (const TypeMember* mTex = find_member(mms, "Texture")) {
Ref tex = read_reference(mat_obj, *mTex);
if (tex.valid()) {
const auto& tms = parse_type(mTex->ref_type);
if (const TypeMember* mFN = find_member(tms, "FromFileName")) {
std::string s = rd_str(member_slot(tex, *mFN));
if (!s.empty()) return s;
}
}
}
// 2) 递归 Maps[].Map(复合材质:granny_material_map = {String Usage; material* Map}
if (const TypeMember* mMaps = find_member(mms, "Maps")) {
Ref b; uint32_t c = 0, s = 0;
if (read_ref_to_array(mat_obj, *mMaps, b, c, s) && c) {
const auto& mapT = parse_type(mMaps->ref_type);
const TypeMember* mSub = find_member(mapT, "Map");
if (!mSub) mSub = find_member(mapT, "Material");
for (uint32_t i = 0; i < c && mSub; ++i) {
Ref e = {b.section, b.offset + i * s};
Ref sub = read_reference(e, *mSub);
std::string r = material_texture_name(sub, mSub->ref_type, depth + 1);
if (!r.empty()) return r;
}
}
}
return {};
}
} // namespace gr2