实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,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
120 lines
5.0 KiB
C++
120 lines
5.0 KiB
C++
// M0 T4 —— FileInfo 汇总(驱动 T5 骨架 / T6 网格 / T7a 曲线分类)。
|
|
// 见 docs/reference/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
|