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,119 @@
|
||||
// M0 T4 —— FileInfo 汇总(驱动 T5 骨架 / T6 网格 / T7a 曲线分类)。
|
||||
// 见 docs/steps/M0-gr2-reader.md
|
||||
// 规格:granny_file_info.cpp (FileInfoType) / granny_animation.cpp (AnimationType)
|
||||
#include "internal.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace gr2 {
|
||||
|
||||
using detail::Ref;
|
||||
using detail::TypeMember;
|
||||
|
||||
bool File::Impl::build_fileinfo(FileInfo& out, LoadError* err) {
|
||||
if (!root_obj.valid() || !root_type.valid()) {
|
||||
if (err) *err = {"fileinfo", "no root object / type"};
|
||||
return false;
|
||||
}
|
||||
const auto& rms = parse_type(root_type);
|
||||
if (rms.empty()) {
|
||||
if (err) *err = {"fileinfo", "root type has no members"};
|
||||
return false;
|
||||
}
|
||||
|
||||
auto count_of = [&](const char* name) -> uint32_t {
|
||||
const TypeMember* m = find_member(rms, name);
|
||||
if (!m) return 0;
|
||||
int32_t c = rd_i32(member_slot(root_obj, *m));
|
||||
return (c > 0 && c < (1 << 24)) ? uint32_t(c) : 0;
|
||||
};
|
||||
|
||||
if (const TypeMember* m = find_member(rms, "FromFileName"))
|
||||
out.from_file_name = rd_str(member_slot(root_obj, *m));
|
||||
|
||||
out.texture_count = count_of("Textures");
|
||||
out.material_count = count_of("Materials");
|
||||
out.model_count = count_of("Models");
|
||||
|
||||
// ── 顶层材质表(调试 / 备用查找)────────────────────────────────────
|
||||
if (const TypeMember* mMa = find_member(rms, "Materials")) {
|
||||
auto refs = read_array_of_refs(root_obj, *mMa);
|
||||
const auto& mams = parse_type(mMa->ref_type);
|
||||
const TypeMember* mMN = find_member(mams, "Name");
|
||||
for (Ref r : refs) {
|
||||
if (!r.valid()) continue;
|
||||
Material mat;
|
||||
if (mMN) mat.name = rd_str(member_slot(r, *mMN));
|
||||
mat.diffuse_texture = material_texture_name(r, mMa->ref_type, 0);
|
||||
out.materials.push_back(std::move(mat));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 骨架(T5)────────────────────────────────────────────────────
|
||||
skels_cache.clear();
|
||||
skel_refs.clear();
|
||||
if (const TypeMember* mSk = find_member(rms, "Skeletons")) {
|
||||
auto refs = read_array_of_refs(root_obj, *mSk);
|
||||
for (Ref r : refs) {
|
||||
Skeleton sk;
|
||||
if (extract_skeleton(r, mSk->ref_type, sk)) {
|
||||
skels_cache.push_back(std::move(sk));
|
||||
skel_refs.push_back(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 模型 → 骨架关联:把 model.InitialPlacement 写进对应 skeleton ─────
|
||||
// (很多 Metin2 骨架把 model 空间偏移放在 InitialPlacement 而非 root 骨骼的
|
||||
// LocalTransform;不带上它,bind pose 自洽检查会对这些文件失败)
|
||||
if (const TypeMember* mMo = find_member(rms, "Models")) {
|
||||
auto refs = read_array_of_refs(root_obj, *mMo);
|
||||
const auto& moms = parse_type(mMo->ref_type);
|
||||
const TypeMember* mSkRef = find_member(moms, "Skeleton");
|
||||
const TypeMember* mIP = find_member(moms, "InitialPlacement");
|
||||
for (Ref r : refs) {
|
||||
if (!r.valid() || !mSkRef) continue;
|
||||
Ref sref = read_reference(r, *mSkRef);
|
||||
for (size_t k = 0; k < skel_refs.size(); ++k) {
|
||||
if (skel_refs[k].section == sref.section && skel_refs[k].offset == sref.offset) {
|
||||
if (mIP) {
|
||||
uint32_t fl = 0;
|
||||
transform_to_composite(member_slot(r, *mIP), skels_cache[k].initial_placement, fl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.skeletons = skels_cache;
|
||||
|
||||
// ── 网格(T6)────────────────────────────────────────────────────
|
||||
if (const TypeMember* mMe = find_member(rms, "Meshes")) {
|
||||
auto refs = read_array_of_refs(root_obj, *mMe);
|
||||
for (Ref r : refs) {
|
||||
Mesh me;
|
||||
bool ok = extract_mesh(r, mMe->ref_type, skels_cache, me);
|
||||
if (getenv("GR2_DEBUG"))
|
||||
fprintf(stderr, "[gr2] mesh ref (%d,%u) -> %s name=%s verts=%zu idx=%zu\n",
|
||||
r.section, r.offset, ok ? "OK" : "FAIL", me.name.c_str(),
|
||||
me.vertices.size(), me.indices.size());
|
||||
if (ok) out.meshes.push_back(std::move(me));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 动画(T7a:名字 + 时长 + 每 track 的曲线子类型)─────────────────
|
||||
if (const TypeMember* mAn = find_member(rms, "Animations")) {
|
||||
auto refs = read_array_of_refs(root_obj, *mAn);
|
||||
const Skeleton* sk0 = skels_cache.empty() ? nullptr : &skels_cache[0];
|
||||
for (Ref r : refs) {
|
||||
if (!r.valid()) continue;
|
||||
Animation an;
|
||||
if (extract_animation(r, mAn->ref_type, sk0, an))
|
||||
out.animations.push_back(std::move(an));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gr2
|
||||
Reference in New Issue
Block a user