fix(net): 主角 VID 归参考端所有,GC_CHARACTER_DEL 不再把玩家「去主角化」
实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,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
This commit is contained in:
@@ -15,9 +15,57 @@ godot --path project --script gamescene_visual_test.gd -- --c1
|
||||
- `gamescene_test.gd` 使用不存在的地图,预期有加载错误日志;它只验证无地图装配,不能当作画面正常证据。
|
||||
- `gamescene_visual_test.gd` 使用真实资产和实际 GameScene,通过 FakeClient 延迟提供 race=0 主角数据,**不发**主角信号。无账号和服务器写入。
|
||||
- 默认先加载 A1;`--c1` 随后提供 C1 服务器位置,用来验证晚到坐标能纠正地图选择。
|
||||
- `gamescene_visual_test.gd` 除位置/地图/表面数外还断言 `feet_gap`:把动画冻结在 0.5 s
|
||||
后取全部 MeshInstance3D 世界包围盒的最低点,减去该处地形高度,要求 |gap| ≤ 0.03 m
|
||||
(R2-01 悬空修复的数值守卫;A1 与 C1 实测均为 0.001373 m)。报告里同时记录
|
||||
`feet_y` / `feet_gap` / `feet_gap_max`。
|
||||
- 视觉用例输出 `build/rendering/gamescene-<timestamp>/game.png` 与 `report.json`。可用环境变量 `MT_RENDER_OUTPUT` 指定独立目录;复用路径会覆盖同名报告,建议每次新目录。
|
||||
- 报告包含位置、相机、地图加载计数、模型表面数、引擎、关键脚本/扩展/模型路径哈希。资产依赖尚未穷举;这只是 R0 部分基线。
|
||||
- 截图尺寸为 1280×720,角色待机固定在 0.5 秒。地形/环境动态因素尚未完全冻结,因此暂不设置像素误差硬阈值。
|
||||
- 测试不配置学习技能,截图中的空快捷槽不代表技能图标验证成功。
|
||||
|
||||
## 不启动游戏的模型渲染回归(model_render_test.gd)
|
||||
|
||||
```bash
|
||||
godot --path project --script model_render_test.gd # 八职业,判定
|
||||
godot --path project --script model_render_test.gd -- --race 0
|
||||
godot --path project --script model_render_test.gd -- --bless # 重建基线
|
||||
```
|
||||
|
||||
无地图、无网络、无服务器:直接用 `ui/player_view.gd` 离线装配一个角色,`wait`
|
||||
动画冻结在 0.5 s,固定灯光(平行光 `(-42,155,0)` energy 1.1 + 环境色
|
||||
`(0.6,0.62,0.68)` energy 0.8)、洋红背景、512×512。与 `char_bench_test.gd` 的区别
|
||||
是它对像素算指标并断言,退出码即结论;bench 仍然只产图供人看。
|
||||
|
||||
指标(每个 race)与判定方式:
|
||||
|
||||
| 指标 | 含义 | 判定 |
|
||||
| --- | --- | --- |
|
||||
| `feet_gap_m` | 最低蒙皮顶点到 y=0 | 绝对,\|gap\| ≤ 0.03 m(实测 −0.0007…0.0030) |
|
||||
| `silhouette_px` | 正面剪影像素数 | 绝对 ≥ 3000;且对基线 ±3% |
|
||||
| `head_cover` | 近景机位下发型盖住头部像素的比例(只统计 no_hair 里属于人体的像素) | 对基线 ±0.03 |
|
||||
| `hair_mean_lum` | 只画发型时发型像素平均亮度 | 对基线 ±0.006 |
|
||||
| `hair_mean_lum_ctrl` | 同上,但换回 R2-02 修复前的 `StandardMaterial3D(roughness=1)` | 负对照:必须比生产值高 ≥ 0.008 |
|
||||
| `hair_lum_p99` / `hair_bright_frac` | 亮度 99 分位 / 超过 0.20 的像素占比 | 仅记录,不判定 |
|
||||
|
||||
为什么用平均亮度而不是「有多少像素炸白」:发色本身差别极大(武士近黑
|
||||
`mean 0.061`,修罗银白 `mean 0.260`),任何跨职业的绝对亮度上限都不成立。平均
|
||||
亮度对「整片发型被额外高光抬亮」敏感,基准差异由按职业记录的基线吸收。
|
||||
|
||||
负对照不是摆设:它每次都真的用旧材质再渲一遍,保证这条指标现在仍分得开好坏
|
||||
(例如有人把灯光调暗到两边都测不出高光时会直接失败)。八职业实测旧材质相对
|
||||
新材质的抬升是 +0.0109 … +0.0371,最小值 0.0109 给阈值 0.006/0.008 留了余量。
|
||||
|
||||
已验证它确实能抓到目标缺陷:把 `Metin2Model::_load_hair()` 临时改回
|
||||
`StandardMaterial3D(roughness=1)` 重新编译后,**八个职业全部失败**
|
||||
(`hair_mean_lum` 偏离基线 0.0109…0.0371);改回 `mtgodot::make_material` 后
|
||||
`PASS`。
|
||||
|
||||
基线在 `test/rendering/model_baseline.json`,含引擎版本。改模型、改发型材质或改
|
||||
这套灯光都会让基线失效,需要人工看过渲染结果再 `--bless`,并在
|
||||
`docs/RENDERING-REPAIR-PLAN.md` 的执行记录里写清重建原因。
|
||||
|
||||
覆盖边界:只有离线单角色装配。不含地图、地形接触、真实光照昼夜、装备叠加、
|
||||
40250 像素比对,也不能替代 `gamescene_visual_test.gd` 与导出包验收。
|
||||
|
||||
已知后续:脸部/脚底近景和区域差异器、完整资产哈希、四职业 GR2/GLB 对照、技能状态与图标用例、真实联机及普通 macOS `.app` 验收。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"engine": "4.7.1-stable (official)",
|
||||
"note": "godot --path project --script model_render_test.gd -- --bless",
|
||||
"races": {
|
||||
"0": {
|
||||
"body_gr2": "warrior_novice.gr2",
|
||||
"hair_bright_frac": 0.0224071972085593,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.254901960784314,
|
||||
"hair_mean_lum": 0.0607960189162477,
|
||||
"hair_mean_lum_ctrl": 0.097848223242717,
|
||||
"head_cover": 0.324674209407673,
|
||||
"head_px": 46579,
|
||||
"silhouette_px": 35073
|
||||
},
|
||||
"1": {
|
||||
"body_gr2": "assassin_novice.gr2",
|
||||
"hair_bright_frac": 0.316447908121411,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.349019607843137,
|
||||
"hair_mean_lum": 0.184415208398222,
|
||||
"hair_mean_lum_ctrl": 0.195338585878083,
|
||||
"head_cover": 0.244265393733581,
|
||||
"head_px": 62428,
|
||||
"silhouette_px": 36052
|
||||
},
|
||||
"2": {
|
||||
"body_gr2": "sura_novice.gr2",
|
||||
"hair_bright_frac": 0.834898912836162,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.435294117647059,
|
||||
"hair_mean_lum": 0.260437632997124,
|
||||
"hair_mean_lum_ctrl": 0.281076636261981,
|
||||
"head_cover": 0.0281083316817606,
|
||||
"head_px": 116051,
|
||||
"silhouette_px": 39682
|
||||
},
|
||||
"3": {
|
||||
"body_gr2": "shaman_novice.gr2",
|
||||
"hair_bright_frac": 0.122538024274082,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.32156862745098,
|
||||
"hair_mean_lum": 0.134457749300365,
|
||||
"hair_mean_lum_ctrl": 0.166435011552631,
|
||||
"head_cover": 0.295606972434941,
|
||||
"head_px": 60983,
|
||||
"silhouette_px": 36257
|
||||
},
|
||||
"4": {
|
||||
"body_gr2": "warrior_novice.gr2",
|
||||
"hair_bright_frac": 0.0294142102856193,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.23921568627451,
|
||||
"hair_mean_lum": 0.0852886966119754,
|
||||
"hair_mean_lum_ctrl": 0.107547303626061,
|
||||
"head_cover": 0.190623521275434,
|
||||
"head_px": 27473,
|
||||
"silhouette_px": 27104
|
||||
},
|
||||
"5": {
|
||||
"body_gr2": "assassin_novice.gr2",
|
||||
"hair_bright_frac": 0.0384454367848172,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.270588235294118,
|
||||
"hair_mean_lum": 0.0734998604521351,
|
||||
"hair_mean_lum_ctrl": 0.0947519741918537,
|
||||
"head_cover": 0.25690174015439,
|
||||
"head_px": 30572,
|
||||
"silhouette_px": 24776
|
||||
},
|
||||
"6": {
|
||||
"body_gr2": "sura_novice.gr2",
|
||||
"hair_bright_frac": 0.905465795806119,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.427450980392157,
|
||||
"hair_mean_lum": 0.277393012894398,
|
||||
"hair_mean_lum_ctrl": 0.298905357949299,
|
||||
"head_cover": 0.617005584237699,
|
||||
"head_px": 37964,
|
||||
"silhouette_px": 30280
|
||||
},
|
||||
"7": {
|
||||
"body_gr2": "shaman_novice.gr2",
|
||||
"hair_bright_frac": 0.0142828216749996,
|
||||
"hair_gr2": "hair_1_1.gr2",
|
||||
"hair_lum_p99": 0.219607843137255,
|
||||
"hair_mean_lum": 0.0624432979584058,
|
||||
"hair_mean_lum_ctrl": 0.0774457732565792,
|
||||
"head_cover": 0.715665995318709,
|
||||
"head_px": 36742,
|
||||
"silhouette_px": 21953
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user