实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,2D UI 与小地图正常。 根因(已实证,非推测)。`EntityStore::mut_despawn()` 在 despawn 的 vid 等于 主角 vid 时把 `m_main_vid` 清零。40250 服务器在 spawn-in 头几秒会对玩家自己的 vid 反复下发 GC_CHARACTER_DEL + GC_CHARACTER_ADD(视野重算,实测约 10 次)。 第一次 DEL 就把玩家永久去主角化:`get_main_vid()` 返回 0 → `_sync_main_character()` 直接 return → 建模代码从未跑过(main_attempts=0)→ 占位胶囊留在 y=0 → 相机跟着它钻到 C1 地形底下 → 满屏灰紫。地图其实一直是好的 (chunks_built 20/20、objects_placed 565)。 参考端怎么做(40250 ClientVS22)。主角身份属于 `CNetworkActorManager::m_dwMainVID`, 只由 `SetMainActorVID()`(GC_MAIN_CHARACTER)写,只在管理器析构时清; `RemoveActor()` 删 actor 行和角色实例,但绝不碰它;重新加入时 `__AppendCharacterManagerActor()` 用 `kCreateData.m_isMain = __IsMainActorVID(dwVID)` 重新推导,并再跑一次 `SetMainCharacterIndex(dwVID)`。 修法(entity_store.cpp): - `mut_despawn()` 只删行、只发 Despawn,不动 m_main_vid;m2dev 分支的 GC_CHARACTER_DEL 同规则。 - `mut_spawn_full()` 在 `e.vid == m_main_vid` 时把 `is_main` 置回 true, 并重新广播 MainSet,对齐参考端的重新推导。 顺带落地的渲染修复(同一次实机排查中定位): - libgr2 `sample_pose()` 增加 `root_offset` 形参;`Metin2AnimPlayer::apply_pose()` 显式传单位阵而不是模型的 InitialPlacement。Metin2 动画 gr2 的根骨轨道自带骨盆 高度,再乘一次 IP 会把整个人抬高一个骨盆高(武 99.4cm / 刺 92.6 / 巫 103.7 / 萨 95.5,正好各自的 IP.z)——即此前的「人物悬空」。`bind_pose()` 语义不变。 - `Metin2Model::_load_hair()` 改用与身体/武器同一条 `make_material()` (specular_disabled)而不是 roughness=1.0 的 StandardMaterial3D,头发不再被 PBR 高光洗成奶白色块。 取证与回归工具: - `project/net_trace.gd`(新):主角 vid 生命周期取证。挂在 AppFlow 之后逐帧读 `get_main_vid()`,归零时打印 `NETTRACE: MAIN_LOST was=.. now=0 reason=..` 加最近 24 条环形事件;`reason` 区分 despawn 与 silent(reset_for_map_change 清空 m_changes,是静默清零)。`MT_NETTRACE=1` 打开逐事件日志。 - `game_scene.gd`:诊断 tick 支持重复采样(分辨主角装配是「没跑」还是「没跑完」), 新增 main_sync_ready / main_loading / main_view_key / main_map_vid / main_retry_in_ms 字段;`MT_DIAG_SHOT=<png>` 让游戏自己存一帧实机画面 (screencapture 拍到的是终端窗口,没用)。 - `project/model_render_test.gd` + `test/rendering/model_baseline.json`(新): 八个种族的模型渲染基线(--bless 重新落盘)。 - `project/char_bench_test.gd`(新):无网络无地图的单角色实验台,五变体 × 六机位出证据图,只产图不做阈值判定。 - `project/package_render_test.gd`(新):导出包内的渲染自检入口 (导出模板不接受 --script,只能走 MT_TEST_MODE)。 - `/project/build/` 加入 .gitignore(渲染用例的落图输出)。 实机复验(真实服务器,非模拟): - 修前 `LIVE_SMOKE FAIL: 主角色 VID 有效`; `NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn t=+7737ms`, 环里紧跟 `spawn vid=25910 is_main=false`。 - 修后 `LIVE_SMOKE RESULT: PASS`,main_vid=25933、entities=19; 40s 实机跑 16 次诊断采样全部 model_built=True / main_attempts=1 / main_sync_ready=True,player_pos 落在 C1 地形上而非 y=0; MT_DIAG_SHOT 存下的帧里地形、建筑、NPC、树木、真实角色模型、HUD、小地图俱全。 回归:gamescene_test / equip_model_test / gamescene_visual_test(feet_gap 0.001373)/ model_render_test(8 races)/ char_select_visual_test / 包内 MT_TEST_MODE=render 全绿;ctest 22/23,唯一失败 net.classic_session 已用 「还原本次改动 → 重跑 → diff 失败集合」确认与本次修改无关(失败集合完全相同)。 注:画面整体偏暗属于光照/色调差距,受 PARITY-GAP.md §0 门禁约束(对照帧未落盘前 不做相机/光照/色调/材质调参),本次不碰。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
275 lines
13 KiB
C++
275 lines
13 KiB
C++
// M0 T7a —— 曲线子类型分类(gr2fuzz 直方图依赖)。T7b(解码)见文末。
|
||
// 见 docs/reference/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),四元数做半球连续化
|
||
// Metin2 的 ScaleShearCurve 是 9 维;旧的 3*4 缓冲区会在这里发生
|
||
// 栈溢出(degree=2 时写入 27 个 float),随后被 __stack_chk_fail 终止。
|
||
// 目前支持的曲线维度为 position=3、orientation=4、scale/shear=9。
|
||
if (dim > 9) return;
|
||
float cp[3 * 9];
|
||
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
|