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