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
+97
View File
@@ -0,0 +1,97 @@
// libgr2 —— gr2 v6 只读读取器(demo 子集)。
// 施工文档:docs/steps/M0-gr2-reader.md
#pragma once
#include "types.h"
#include <cstdint>
#include <cstddef>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace gr2 {
enum class Magic { Unknown, Bit32_LE, Bit32_BE, Bit64_LE, Bit64_BE };
struct LoadError {
std::string stage; // "header" / "section" / "typetree" / "fileinfo" / ...
std::string message;
};
// T3:通用类型树遍历(gr2dump 打印成员用;不硬编码 struct 布局)
struct MemberDump { std::string name; int member_type; int array_width; };
class File;
std::vector<MemberDump> dump_root_members(const File&);
// 材质视图(类型树遍历 root.Materials → granny_material)。
// 只读文件名,不解码贴图。diffuse_texture 取 Usage 含 "diffuse" 的 map 的
// 贴图,退而取第一张;all_textures 是从该材质可达的全部 FromFileName。
struct MaterialInfo {
std::string name;
std::string diffuse_texture;
std::vector<std::string> all_textures;
// Metin2/EterGrnLib 渲染态推断(gr2 不带 D3DRS,靠名字 + map 数 + ExtendedData):
int map_count = 0; // granny_material.MapCount(可达子材质的 Texture 数)
bool alpha_blend = false;// Name 以 "Blend" 开头且 map_count>1EterGrnLib Material.cpp:233
bool two_sided = false; // 名字含 2side/twoside/double/leaf/tree/ivy/fenceExtendedData "Two-sided" 的近似)
};
std::vector<MaterialInfo> dump_materials(const File&);
// 已解析、fixup 后的文件。析构释放所有 section 缓冲。
class File {
public:
static std::optional<File> load(const uint8_t* bytes, size_t len, LoadError* err);
static std::optional<File> load_path(const std::string& path, LoadError* err);
// T1
Magic magic() const { return magic_; }
uint32_t format_version()const { return version_; } // 期望 6
uint32_t total_size() const { return total_size_; }
const std::vector<SectionInfo>& sections() const { return sections_; }
// T4 —— 完整汇总(内部会按需触发 T5/T6/T7a)
const FileInfo& file_info() const { return info_; }
// 展开后的 section 字节(T2 产出)。空 section 返回空 span。调试 / 校验用。
std::span<const uint8_t> section_bytes(size_t i) const;
// T5 —— bind pose 自洽检查(不依赖 oracle)。返回 max|world_bind·invBind I|
// 阈值策略见 M0-gr2-reader.md T5noise_floor 前用 1e-3
double bind_pose_self_check(int skeleton = 0) const;
// M2 便捷封装(同文件内的 skeleton + animation)。跨文件(model gr2 + anim gr2
// 用自由函数 gr2::sample_pose(skeleton, animation, t, ...)。anim/skeleton 是 file_info() 索引。
bool sample_pose(int anim, float t, int skeleton,
std::vector<Mat4>& world_pose, std::vector<Mat4>& skin) const;
bool bind_pose(int skeleton, std::vector<Mat4>& world_pose, std::vector<Mat4>& skin) const;
private:
File() = default;
Magic magic_ = Magic::Unknown;
uint32_t version_ = 0;
uint32_t total_size_ = 0;
std::vector<SectionInfo> sections_;
FileInfo info_;
// 内部:section 缓冲、root object 指针、类型树 —— 放 .cpp 的 Impl
struct Impl;
std::shared_ptr<Impl> impl_;
friend std::vector<MemberDump> dump_root_members(const File&);
friend std::vector<MaterialInfo> dump_materials(const File&);
};
// ── M2:姿势采样(model 的 skeleton + anim 的 tracks 可来自不同 gr2)──────
// Granny 语义、行主序、平移在 elem 12..14
// world[i] = Composite(local[i]) · (parent<0 ? sk.initial_placement : world[parent])
// skin[i] = sk.bones[i].inverse_world · world[i]
// tracks 按骨骼名匹配 sk;无匹配的骨骼用 sk 的 bind LocalTransform。
void sample_pose(const Skeleton& sk, const Animation& an, float t,
std::vector<Mat4>& world, std::vector<Mat4>& skin);
void bind_pose(const Skeleton& sk, std::vector<Mat4>& world, std::vector<Mat4>& skin);
// 打印辅助
const char* magic_name(Magic);
const char* compression_name(uint32_t section_format); // 0=none 1=Oodle0 2=Oodle1
} // namespace gr2
+127
View File
@@ -0,0 +1,127 @@
// libgr2 —— 对外 POD 视图。不泄露内部指针 / SDK 类型。
// 规格来源:MobileSource/Cross Platform/Granny-3D-SDK-main/source/
// granny_file_format.h / granny_data_type_definition.h / granny_transform.h / granny_file_info.h
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <span>
#include <array>
namespace gr2 {
using Mat4 = std::array<float, 16>; // 行主序,Granny 原生。坐标约定见 docs/steps/M0-gr2-reader.md「约定」
// ── 骨架 ──────────────────────────────────────────────────────────────
struct Bone {
std::string name;
int32_t parent = -1; // < 自身索引,或 -1 = 根
Mat4 local_transform{}; // granny_transform SRT → 4x4(组合顺序照 granny_transform.cpp
Mat4 inverse_world{}; // InverseWorld4x4,来自文件
uint32_t lt_flags = 0; // granny_transform.Flags:哪部分非单位
};
struct Skeleton {
std::vector<Bone> bones;
Mat4 initial_placement{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; // 绑定它的 model 的 InitialPlacement
};
// ── 网格 ──────────────────────────────────────────────────────────────
enum class VertexKind { PNT332, PNT3322, PNT332_Skinned, PNT3322_Skinned, Unknown };
struct Vertex { // demo 统一打包结构
float pos[3];
float normal[3];
float uv0[2];
float uv1[2]; // PNT3322 才有效
uint8_t bone_index[4] = {0}; // *_Skinned 才有效
uint8_t bone_weight[4] = {0}; // 归一 u8
};
struct TriGroup {
int32_t material_index;
int32_t tri_first; // 三角序号
int32_t tri_count;
};
struct Mesh {
std::string name;
VertexKind kind = VertexKind::Unknown;
std::vector<Vertex> vertices;
std::vector<uint32_t> indices; // 三角形列表
std::vector<TriGroup> tri_groups;
std::vector<int32_t> bone_bindings; // mesh 局部骨骼槽 → skeleton 骨骼索引
uint32_t source_bone_weight_slots = 0; // 顶点类型声明值;审计 >4/8 权重
uint32_t source_bone_index_slots = 0; // packed UInt32 记为 4
// 与 granny_mesh.MaterialBindings 平行:每项 = 解析出的漫反射贴图文件名
// granny_texture.FromFileName 原样,通常是 "D:\Ymir Work\...\x.dds";空 = 未解析)。
// tri_groups[i].material_index 索引本表。
std::vector<std::string> material_textures;
bool rigid = false; // GrannyMeshIsRigid
};
// ── 动画 ──────────────────────────────────────────────────────────────
// 曲线子类型:T7a 分类。Metin2 = Granny 2.4,全是 OldCurveTypegranny_curve.cpp):
// {Int32 Degree; RefToArray Knots(Real32[]); RefToArray Controls(Real32[])}
// 无压缩变体。label 形如 "Old(d=2,dim=4)"。
enum class CurveFormat { Keyframes32f, /* T7a 跑 fuzz 后补齐 */ Unknown };
// T7b:解出的曲线(B 样条:knots 时间轴 + controls 控制点,行优先 [knot][dim]
struct Curve {
int32_t degree = 0;
uint32_t dim = 0; // 3=vec, 4=quat, 9=3x3 scaleshear
std::vector<float> knots; // KnotCount 个
std::vector<float> controls; // KnotCount * dim 个
bool empty() const { return knots.empty() || dim == 0; }
// t 处求值,写 dim 个 float 到 outidentity 由调用方给)。
void eval(float t, float* out) const;
};
struct BoneTrack {
std::string bone_name;
int32_t bone_index = -1; // 映射到 skeleton(未映射 = -1
std::string pos_type = "none";
std::string rot_type = "none";
std::string scale_type = "none";
Curve position; // dim 3
Curve orientation; // dim 4(四元数 x,y,z,w
Curve scale_shear; // dim 93x3)或 dim 3
};
struct Animation {
std::string name;
float duration = 0.0f;
std::vector<BoneTrack> tracks;
// t 处每个 track 的局部 4x4granny_transform 组合语义,行主序)。
// out 按 tracks 顺序;bone_index 见各 track。
void sample_local(float t, std::vector<Mat4>& out) const;
};
// 顶层 granny_materialdriven by root Materials[])。name + 解析出的漫反射贴图。
struct Material {
std::string name;
std::string diffuse_texture; // granny_texture.FromFileName 原样
};
// ── FileInfo 汇总(T4)────────────────────────────────────────────────
struct FileInfo {
std::string from_file_name;
std::vector<Skeleton> skeletons;
std::vector<Mesh> meshes;
std::vector<Animation> animations;
std::vector<Material> materials;
uint32_t material_count = 0;
uint32_t texture_count = 0;
uint32_t model_count = 0;
};
// ── section 表(T1gr2dump --sections 用)────────────────────────────
struct SectionInfo {
uint32_t compression; // 0=none 1/2=Oodle0/1 4=BitKnit
uint32_t data_size; // 压缩后
uint32_t expanded_size; // 展开后
uint32_t pointer_fixup_count;
int decompress_status; // 0=ok,非 0=解压失败/未实现(T2)
};
} // namespace gr2