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
+270
View File
@@ -0,0 +1,270 @@
// M0 T7a —— 曲线子类型分类(gr2fuzz 直方图依赖)。T7b(解码)见文末。
// 见 docs/steps/M0-gr2-reader.md
// 规格:granny_track_group.cpp (TrackGroupType/TransformTrackType/Curve2Type) /
// granny_curve.cpp (CurveData*Type,均以 CurveDataHeaderType 开头) /
// granny_curve.h (curve_data_header{ u8 Format; u8 Degree })
#include "internal.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ADBG(...) do { if (getenv("GR2_DEBUG")) fprintf(stderr, "[anim] " __VA_ARGS__); } while (0)
namespace gr2 {
using detail::Ref;
using detail::TypeMember;
// Metin2 = Granny 2.4:曲线是 OldCurveType(granny_curve.cpp):
// {Int32 Degree; RefToArray Knots(Real32[]); RefToArray Controls(Real32[])} —— 20B,无压缩变体。
// curve_obj 指向 transform_track 里 inline 的那个 curve 对象本体。
// 新版(2.6+)是 curve2{variant CurveData},本函数一并识别、打标签。
std::string File::Impl::classify_curve_obj(Ref curve_obj, Ref curve_type) {
if (!curve_obj.valid() || !curve_type.valid()) return "none";
const auto& ms = parse_type(curve_type);
if (ms.empty()) return "Unknown";
// 老格式:第一个成员是 "Degree"
if (ms[0].name == "Degree") {
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
int32_t degree = rd_i32(member_slot(curve_obj, *mD));
uint32_t kn = mK ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mK)))) : 0;
uint32_t cn = mC ? uint32_t(std::max(0, rd_i32(member_slot(curve_obj, *mC)))) : 0;
uint32_t dim = kn ? cn / kn : 0;
char buf[64];
std::snprintf(buf, sizeof buf, "Old(d=%d,dim=%u)", degree, dim);
return buf;
}
// 新格式:curve2{variant CurveData}
if (ms[0].name == "CurveData") {
Ref type_ref = follow(member_slot(curve_obj, ms[0])); // variant.Type
if (!type_ref.valid()) return "Identity";
const auto& dm = parse_type(type_ref);
if (dm.empty()) return "Unknown";
const std::string& n = dm[0].name;
const char* pfx = "CurveDataHeader_";
return (n.rfind(pfx, 0) == 0) ? n.substr(std::strlen(pfx)) : n;
}
return "Unknown";
}
// 读一条 OldCurveType 曲线({Int32 Degree; RefToArray Knots; RefToArray Controls})到 Curve。
void File::Impl::read_old_curve(Ref curve_obj, Ref curve_type, Curve& out) {
if (!curve_obj.valid() || !curve_type.valid()) return;
const auto& ms = parse_type(curve_type);
if (ms.empty() || ms[0].name != "Degree") return; // 新格式暂不解
const TypeMember* mD = &ms[0];
const TypeMember* mK = find_member(ms, "Knots");
const TypeMember* mC = find_member(ms, "Controls");
if (!mK || !mC) return;
out.degree = rd_i32(member_slot(curve_obj, *mD));
auto read_farr = [&](const TypeMember& m, std::vector<float>& dst) {
Ref slot = member_slot(curve_obj, m);
int32_t cnt = rd_i32(slot);
if (cnt <= 0 || cnt > (1 << 24)) return;
Ref base = follow({slot.section, slot.offset + 4});
const uint8_t* p = at(base, size_t(cnt) * 4);
if (!p) return;
dst.resize(cnt);
for (int32_t k = 0; k < cnt; ++k) std::memcpy(&dst[k], p + size_t(k) * 4, 4);
};
read_farr(*mK, out.knots);
read_farr(*mC, out.controls);
out.dim = out.knots.empty() ? 0 : uint32_t(out.controls.size() / out.knots.size());
if (out.dim && out.knots.size() * out.dim != out.controls.size()) out.dim = 0; // 不一致 → 弃
// 有些资产的曲线控制点本身是 NaN(如 redthief_general/*_damage.gr2 的 finger track)。
// 不 sanitize 会把 NaN 灌进整条蒙皮链。→ 整条曲线弃(eval 返回空,调用方回退到 bind)。
for (float v : out.knots) if (!std::isfinite(v)) { out.dim = 0; return; }
for (float v : out.controls) if (!std::isfinite(v)) { out.dim = 0; return; }
}
// animation: {String Name; Real32 Duration; Real32 TimeStep; Real32 Oversampling;
// ArrayOfRefs TrackGroups; ...}
// track_group: {String Name; RefToArray VectorTracks; RefToArray TransformTracks; ...}
// transform_track: {String Name; Int32 Flags; Inline OrientationCurve(curve2);
// Inline PositionCurve(curve2); Inline ScaleShearCurve(curve2)}
bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton* skel, Animation& out) {
if (!anim_obj.valid() || !anim_type.valid()) return false;
const auto& ams = parse_type(anim_type);
if (const TypeMember* m = find_member(ams, "Name"))
out.name = rd_str(member_slot(anim_obj, *m));
if (const TypeMember* m = find_member(ams, "Duration"))
out.duration = rd_f32(member_slot(anim_obj, *m));
const TypeMember* mTG = find_member(ams, "TrackGroups");
if (!mTG) return true;
auto groups = read_array_of_refs(anim_obj, *mTG);
const auto& tgms = parse_type(mTG->ref_type);
const TypeMember* mTT = find_member(tgms, "TransformTracks");
if (!mTT) return true;
const auto& ttms = parse_type(mTT->ref_type);
if (getenv("GR2_DEBUG")) {
fprintf(stderr, "[anim] transform_track type members=%zu:\n", ttms.size());
for (auto& m : ttms)
fprintf(stderr, "[anim] %-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);
}
const TypeMember* mName = find_member(ttms, "Name");
const TypeMember* mOri = find_member(ttms, "OrientationCurve");
const TypeMember* mPos = find_member(ttms, "PositionCurve");
const TypeMember* mScl = find_member(ttms, "ScaleShearCurve");
for (Ref g : groups) {
if (!g.valid()) continue;
Ref base; uint32_t count = 0, stride = 0;
if (!read_ref_to_array(g, *mTT, base, count, stride) || count == 0) continue;
for (uint32_t i = 0; i < count; ++i) {
Ref t = {base.section, base.offset + i * stride};
BoneTrack bt;
if (mName) bt.bone_name = rd_str(member_slot(t, *mName));
if (mPos) {
bt.pos_type = classify_curve_obj(member_slot(t, *mPos), mPos->ref_type);
read_old_curve(member_slot(t, *mPos), mPos->ref_type, bt.position);
}
if (mOri) {
bt.rot_type = classify_curve_obj(member_slot(t, *mOri), mOri->ref_type);
read_old_curve(member_slot(t, *mOri), mOri->ref_type, bt.orientation);
}
if (mScl) {
bt.scale_type = classify_curve_obj(member_slot(t, *mScl), mScl->ref_type);
read_old_curve(member_slot(t, *mScl), mScl->ref_type, bt.scale_shear);
}
if (skel) for (size_t k = 0; k < skel->bones.size(); ++k)
if (skel->bones[k].name == bt.bone_name) { bt.bone_index = int32_t(k); break; }
out.tracks.push_back(std::move(bt));
}
}
return true;
}
// ── T7b:曲线解码 ─────────────────────────────────────────────────────
// Metin2 曲线全是 OldCurveType(B 样条:Degree + Knots[] + Controls[knot*dim])。
// 求值照 granny_bspline.cpp SampleBSpline{0,1,2}xN + granny_bspline_inlines.h
// {Linear,Quadratic}Coefficients。degree 0=常量,1=线性,2=二次 B 样条。
// KnotIndex = "第一个 > t 的 knot"(granny CurveFindKnot),窗口越界按端点 clamp。
//
// dim==4(四元数):blend 之前对本 span 参与的 degree+1 个控制点做
// EnsureQuaternionContinuity(granny_math.cpp)—— 逐点若与前一点点积 < 0 则整体取反。
// 少了这一步,跨半球(q vs -q)的控制点线性混合会收缩到近零,归一后得到一个
// 合法但方向错约 180° 的旋转(表情动作里脖子/头会折叠塌陷)。
void Curve::eval(float t, float* out) const {
const uint32_t n = (uint32_t)knots.size();
if (n == 0 || dim == 0) return;
auto Kc = [&](int i) -> float {
i = i < 0 ? 0 : (i >= (int)n ? (int)n - 1 : i);
return knots[(uint32_t)i];
};
if (n == 1 || degree <= 0) { // 常量
for (uint32_t d = 0; d < dim; ++d) out[d] = controls[d];
return;
}
if (t <= knots[0]) t = knots[0];
if (t >= knots[n - 1]) t = knots[n - 1];
int i = (int)n - 1;
for (uint32_t k = 0; k < n; ++k) if (knots[k] > t) { i = (int)k; break; }
if (i < 1) i = 1;
const int span = (degree == 1) ? 2 : 3; // 本 span 的控制点数 = degree+1
const int first = (degree == 1) ? (i - 1) : (i - 2);
// 拷本 span 的控制点(越界按端点 clamp),四元数做半球连续化
float cp[3 * 4]; // 最多 3 点 × dim4
for (int s = 0; s < span; ++s) {
int ci = first + s;
ci = ci < 0 ? 0 : (ci >= (int)n ? (int)n - 1 : ci);
for (uint32_t d = 0; d < dim; ++d) cp[s * dim + d] = controls[(uint32_t)ci * dim + d];
}
if (dim == 4) {
for (int s = 1; s < span; ++s) {
const float* p = &cp[(s - 1) * 4];
float* q = &cp[s * 4];
if (p[0]*q[0] + p[1]*q[1] + p[2]*q[2] + p[3]*q[3] < 0.0f)
for (int d = 0; d < 4; ++d) q[d] = -q[d];
}
}
if (degree == 1) {
float denom = Kc(i) - Kc(i - 1);
float c0 = denom != 0.0f ? (t - Kc(i - 1)) / denom : 0.0f;
c0 = c0 < 0 ? 0 : (c0 > 1 ? 1 : c0);
float c1 = 1.0f - c0;
for (uint32_t d = 0; d < dim; ++d)
out[d] = c1 * cp[0 * dim + d] + c0 * cp[1 * dim + d];
} else { // degree >= 2 → 二次(Metin2 无 degree 3)
float ti_2 = Kc(i - 2), ti_1 = Kc(i - 1), ti = Kc(i), ti1 = Kc(i + 1);
auto fdiv = [](float a, float b) { return b != 0.0f ? a / b : 0.0f; };
float L0 = fdiv(t - ti_1, ti - ti_1);
float L1_1 = fdiv(t - ti_2, ti - ti_2);
float L1_2 = fdiv(t - ti_1, ti1 - ti_1);
float tmp = (L1_1 + L0) - L0 * L1_1;
float c0 = L0 * L1_2; // ci
float c1 = tmp - c0; // ci_1
float c2 = 1.0f - tmp; // ci_2
for (uint32_t d = 0; d < dim; ++d)
out[d] = c2 * cp[0 * dim + d] + c1 * cp[1 * dim + d] + c0 * cp[2 * dim + d];
}
if (dim == 4) { // 四元数:归一
float m = std::sqrt(out[0]*out[0] + out[1]*out[1] + out[2]*out[2] + out[3]*out[3]);
if (m > 1e-12f) for (int d = 0; d < 4; ++d) out[d] /= m;
}
}
// t 处每个 track 的局部 4x4(BuildCompositeTransform4x4 语义:C 上 3x3 = transpose(R3*SS3),
// 平移在 elem 12..14)。position 缺省 (0,0,0),orientation 缺省单位四元数,scaleshear 缺省 I。
void Animation::sample_local(float t, std::vector<Mat4>& out) const {
out.assign(tracks.size(), Mat4{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1});
for (size_t ti = 0; ti < tracks.size(); ++ti) {
const BoneTrack& tr = tracks[ti];
float pos[3] = {0, 0, 0};
float ori[4] = {0, 0, 0, 1};
float ss[9] = {1,0,0, 0,1,0, 0,0,1};
if (!tr.position.empty()) tr.position.eval(t, pos);
if (!tr.orientation.empty()) tr.orientation.eval(t, ori);
bool have_ss = false;
if (!tr.scale_shear.empty()) {
if (tr.scale_shear.dim == 9) { tr.scale_shear.eval(t, ss); have_ss = true; }
else if (tr.scale_shear.dim == 3) {
float s[3]; tr.scale_shear.eval(t, s);
ss[0] = s[0]; ss[4] = s[1]; ss[8] = s[2]; have_ss = true;
}
}
// R3 (行主序) from quat
const float xx = ori[0]*ori[0], yy = ori[1]*ori[1], zz = ori[2]*ori[2];
const float xy = ori[0]*ori[1], xz = ori[0]*ori[2], yz = ori[1]*ori[2];
const float wx = ori[3]*ori[0], wy = ori[3]*ori[1], wz = ori[3]*ori[2];
float r3[9] = {
1 - 2*(yy+zz), 2*(xy-wz), 2*(xz+wy),
2*(xy+wz), 1 - 2*(xx+zz), 2*(yz-wx),
2*(xz-wy), 2*(yz+wx), 1 - 2*(xx+yy)
};
float u3[9];
if (have_ss) {
for (int r = 0; r < 3; ++r)
for (int c = 0; c < 3; ++c)
u3[r*3+c] = r3[r*3+0]*ss[0*3+c] + r3[r*3+1]*ss[1*3+c] + r3[r*3+2]*ss[2*3+c];
} else {
for (int k = 0; k < 9; ++k) u3[k] = r3[k];
}
Mat4& m = out[ti];
m = Mat4{};
m[0] = u3[0]; m[1] = u3[3]; m[2] = u3[6]; m[3] = 0;
m[4] = u3[1]; m[5] = u3[4]; m[6] = u3[7]; m[7] = 0;
m[8] = u3[2]; m[9] = u3[5]; m[10] = u3[8]; m[11] = 0;
m[12] = pos[0]; m[13] = pos[1]; m[14] = pos[2]; m[15] = 1;
}
}
} // namespace gr2