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,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>1(EterGrnLib Material.cpp:233)
|
||||
bool two_sided = false; // 名字含 2side/twoside/double/leaf/tree/ivy/fence(ExtendedData "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 T5(noise_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
|
||||
@@ -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,全是 OldCurveType(granny_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 到 out(identity 由调用方给)。
|
||||
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 9(3x3)或 dim 3
|
||||
};
|
||||
|
||||
struct Animation {
|
||||
std::string name;
|
||||
float duration = 0.0f;
|
||||
std::vector<BoneTrack> tracks;
|
||||
// t 处每个 track 的局部 4x4(granny_transform 组合语义,行主序)。
|
||||
// out 按 tracks 顺序;bone_index 见各 track。
|
||||
void sample_local(float t, std::vector<Mat4>& out) const;
|
||||
};
|
||||
|
||||
// 顶层 granny_material(driven 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 表(T1,gr2dump --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
|
||||
Reference in New Issue
Block a user