Files
mtgodot-poc/docs/FIRST-MAC-PLAYABLE-IMPLEMENTATION.md

276 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 首个 Mac 联网内测版:开发实施文档
日期:2026-09-11。范围:Apple Silicon / arm64 Release。
本文将联网闭环、基础战斗、首测地图和稳定性四项需求拆为可开发任务。本文新增的文件、API、环境变量均为**待实现设计**,不是已存在功能;列为“现有”的入口已按当前工作树核对。本轮只编写文档,不执行联网测试或修改运行时代码。
## 1. 基线与交付边界
现有候选包:`build/export-native-trees-20260911/mtgodot-poc.app`。已通过离线包内加载及干净退出;树木专项覆盖 87 种资源、六图 2092 棵树,不能代替联网或树怪验收。
首版放行不是“渲染全部完成”。必须交付:固定哈希的候选包、实际联网闭环证据、基础战斗矩阵、首测地图截图、稳定性报告和已知问题。风动、逐叶朝向、SpeedTree 原生 LOD、40250 全环境像素一致性可延期。
首测地图拟为出生城镇及练级区、鬼木林、赤鬼木林。开发前通过资源和实际服务器确定 map key、进入坐标、合法路线、角色槽位与怪物刷新;不得凭中文名硬编码地图路径。测试角色等级、装备、技能、掉落条件需由测试环境负责人准备;没有条件时报告 BLOCKED,禁止伪造事件或偷偷修改服务器。
## 2. 已有实现与明确缺口
| 现有文件 / API | 可复用部分 | 本轮必须补齐 |
| --- | --- | --- |
| `script/live_smoke_test.sh` | 指定 MT_SMOKE_APP、凭据输入、报告与进程码 | 地址预检写死;无完整退出错误门禁;缺墙钟超时和战斗流程 |
| `project/live_smoke_test.gd` | setup、状态机、世界读取、移动、聊天、reconnect | 发包成功不是服务端结果;无完整打怪拾取链 |
| `project/client_main.gd` | 包内 MT_TEST_MODE=smoke/render 分流 | 增加 playable、地图视觉入口;普通启动不启用测试 |
| `project/app_flow.gd` | start、state、登录/选角/进场和断线生命周期 | 提供只读场景快照,避免测试遍历私有节点 |
| `project/net_play.gd` | set_attack_key、pick_ground_item、skill_context | 提取公共选目标入口,沿用真实战斗逻辑 |
| `project/ui/quickbar.gd` | activate、assign、restore_from_serverskill_cast_started、skill_rejected | 输入、服务端结果与视觉证据关联 |
| `project/player_skill.gd` | use_skill、可用性与目标检查 | 新增负向/生命周期覆盖,不另造施法实现 |
| `project/ui/mob_view.gd` | build、set_anim_state、motlist 映射 | 树怪矩阵及禁止静默动作回退的验收 |
| `project/rendering_scenario_test.gd` | 离线 CPU/GPU 多角色、帧时间、内存计数 | P99、事件关联、RSS、长期联网负载;现有假客户端不是实网证据 |
| `script/package_render_test.sh` | 进程退出后扫描完整日志 | 抽取通用门禁供联网/稳定性使用 |
当前 `mob_winding_test.gd` 只覆盖 101、110、20001,不覆盖树怪。`native_tree_test.gd` 测的是静态场景树,不能复用其 PASS 证明怪物蒙皮正确。
## 3. 开发拆分与依赖
每个任务需单独可审查 diff,先添加失败用例再修复;工作树已有修改必须保留,不把无关修改混进任务结论。
| ID | 开发内容 | 依赖 | 完成产物 |
| --- | --- | --- | --- |
| INF-01 | 场景清单、报告协议、事件采集、配置校验 | 无 | 配置负测及报告协议测试 |
| INF-02 | 通用包运行器、墙钟超时、退出门禁 | INF-01 | 假进程门禁测试 |
| NET-01 | 包内真实联网状态机、只读诊断接口 | INF-01/02 | 3 次真实闭环 |
| CBT-01 | 技能/普攻输入路径与证据关联 | NET-01 | 四职业矩阵 |
| MAP-01 | 地图/树怪资源审计与离线 Metal 截图 | INF-01,可与 NET 并行 | 资源清单、动作截图 |
| MAP-02 | 实际地图定位、战斗、落地检查 | NET-01、MAP-01 | 首测地图验收 |
| STB-01 | 采样、循环场景、故障与窗口测试 | NET-01、CBT-01、MAP-02 | 2 小时报告 |
| REL-01 | 重打包、聚合门禁、已知问题 | 全部 | 可追溯内测包 |
实际运行缺陷的修复范围由证据决定;不能预先将未知 bug 标成已定位。缺陷修复卡必须写出:失败用例 ID、资源/角色/地图、原因、修改文件、修复前后证据、回归集合。
## 4. INF-01:测试基础设施的数据契约
### 4.1 新增文件
- `test/playable/scenario.example.json`:无账号密码的模板,未解析的地图/技能字段留空并拒绝运行。
- `project/testing/playable_config.gd`:验证配置、场景与资源前置条件。
- `project/testing/playable_report.gd`:统一检查、事件记录与最终报告。
- `project/testing/playable_probe.gd`:只读订阅生产对象,不发包、不写实体数据。
- `project/playable_harness_test.gd`:假客户端验证状态机和证据判定,不作为实网验收。
配置采用 schema_version=1;包含 scenario_id、protocol、serverinfo 中选定地址、character_slot、map_key、waypoints_cm、allowed_mob_vnums、allowed_drop_vnums、skill_cases、resolution、loops、timeout_seconds。所有坐标字段显式带单位,网络到世界换算沿用生产代码;不把 Vector2/Vector3 强转用于凑通过。路线必须预先人工确认安全可达。
账号密码仅通过现有 MT_ACCOUNT/MT_PASSWORD 或安全交互输入提供;禁止写入 JSON、命令行参数、截图或报告。报告不复制进程环境。启动脚本不能使用 set -x。日志在落盘前脱敏,测试账号名也不写入最终公开报告。
### 4.2 报告契约
`report.json` 必须具有如下字段:
```json
{
"schema_version": 1,
"run_id": "unique-run-id",
"suite": "playable",
"status": "BLOCKED",
"build": {"engine_sha256": "...", "extension_sha256": "...", "pck_sha256": "...", "arch": "arm64"},
"environment": {"os": "...", "renderer": "...", "resolution": [1280, 720]},
"cases": [{"id": "NET-ATTACK-01", "status": "BLOCKED", "duration_ms": 0, "reason": "fixture_not_ready", "evidence": []}],
"failures": [],
"blocked": ["fixture_not_ready"],
"coverage": {"required": 1, "passed": 0},
"exit_gate": {"checked": false, "process_code": null, "errors": []}
}
```
状态限定 PASS/FAIL/BLOCKED/SKIP;必测项 BLOCKED、SKIP 均不得放行。未知 schema、空 cases、缺哈希、证据文件不存在、required 与清单不一致均失败。客户端只能写 `client-report.json`,父运行器在真实退出后生成最终 `report.json`,防止退出前 PASS 掩盖泄漏。
事件写 `events.jsonl`,每条含 monotonic_us、run_id、case_id、connection_epoch、stage、kind、actor_vid、target_vid、payload(白名单字段)。重连后 VID 可变化,按角色身份与新 epoch 重新绑定,禁止等待旧 VID。检测“来自服务端”的依据必须是接收端分发,而非本地预测信号。
### 4.3 必须先有的负向测试
无凭据/坏配置、空覆盖、旧报告、错 run_id、报告 PASS 但进程失败、PASS 后输出 RID 警告、只有本地动画无服务端事件、重连后旧事件、超时不退出,均应返回非零。任何缺测都不能降级成 PASS。
## 5. INF-02:统一运行与退出门禁
新增 `script/run_client_gate.sh``script/validate_playable_report.mjs`,由 `script/playable_test.sh` 调用;复用 `package_render_test.sh` 的错误匹配规则,不弱化现有门禁。
1. 每次创建唯一输出目录,记录本次 run_id 和包哈希;不复用历史报告。
2. 校验包可执行、架构和签名;清除 MT_ASSETS,保证验收用包内资源。
3. 预检地址与 `project/net/serverinfo.gd` 的实际选中配置一致。新增测试配置覆盖只能在测试模式生效;删除冒烟脚本预检地址与客户端连接地址分叉的风险。
4. 只管理本运行器启动的子进程 PID;超时先 TERM,宽限 10 秒后 KILL,再 wait 收集状态。不得 pkill 所有客户端或修改全机网络。
5. 读取完整标准输出/错误,检查 SCRIPT ERROR、ERROR、leaked at exit、shaders never freed。无过滤 RID 警告的允许名单。
6. 同时满足进程 0、报告协议正确、全部必测 PASS、完整日志门禁通过,才写最终 PASS。
建议退出码:0=PASS,1=断言或退出门禁失败,2=配置/前置条件阻塞,124=墙钟超时。进程原始退出码另存报告,不丢失。
门禁单测使用脚本生成的假子进程输出,不需游戏服务器。确认超时分支、信号退出和 stdout 管道不会绕过真实进程码。
## 6. NET-01:真实联网闭环状态机
### 6.1 修改点与接口
- 新增 `project/playable_live_test.gd`,实现 `setup(flow: Node, client: Node, config: Dictionary)`
- `client_main.gd` 增加 `MT_TEST_MODE=playable` 分支,在 AppFlow 建立后挂载该 Node;不得导出后用 --script 替代包内入口。
- `app_flow.gd` 新增只读 `get_playable_snapshot() -> Dictionary`,返回 stage、角色身份摘要、scene_ready 和当前地图。测试对象引用通过明确的测试 adapter 获取,不把 Node 放入 JSON。
- `game_scene.gd` 新增只读 `get_playable_context() -> Dictionary`,在测试模式提供现有 net_play、quickbar、net_world、world、player 的引用;场景销毁时失效。
- `net_play.gd` 新增公共 `select_target(vid: int) -> bool`,复用现有 `_change_target_to_picked_instance``_on_pick` 的共同目标选择逻辑;保留距离、死亡和可攻击检查,不直接改私有字段。
先核实 M2Client 信号签名与 classic 收包源;缺少来源可辨识证据时,向现有接收分发处增加测试诊断通知,不添加假业务事件,不改变协议格式。
### 6.2 状态及判定
| 状态 | 动作 | 成功证据 | 默认超时 |
| --- | --- | --- | --- |
| LOGIN | 复用 AppFlow 自动登录 | char_list 接收且指定槽位存在 | 30s |
| SELECT | 现有选角流程 | entered_game + 主实体有效 | 45s |
| WORLD_READY | 等待场景加载 | map key 符合、地图与角色就绪、HUD 可用 | 60s |
| MOVE | 通过玩家控制器走配置路线 | 本地到达 + 无服务端纠正;重登/服务端观察验证落点 | 20s/点 |
| TARGET | select_target 选择允许范围怪物 | 当前 epoch 中存活实体、race 合法 | 30s |
| ATTACK | set_attack_key(true),结束/失败必释放 | 指定目标接收血量/伤害变化与死亡结果 | 90s |
| DROP | 观察真实掉落 | 新地面物品、允许 vnum、可拾取归属和邻近位置 | 30s |
| PICKUP | pick_ground_item(iid) | 地面消失 + 背包对应 vnum 总量增加 | 15s |
| EXIT | 正常退出入口 | 父运行器检查真实退出和完整日志 | 20s |
MOVE 不要求服务器必然回显主角每一个移动包;classic 若无回显,使用重登位置或服务器/第二观察客户端确认,不把本地 position 变化叫做服务端确认。若没有可用证据,标记该断言 BLOCKED。
拾取比较所有背包格的 vnum 总量(支持堆叠),并关联时间窗和地面 iid;只看到地面删除不能证明由自己拾取。金钱掉落另测点数,不套背包断言。无掉落的随机战斗不能无限重试:测试环境准备确定可拾取掉落,最多 3 次允许怪物击杀,无条件则 BLOCKED。
每个状态入口记录快照,失败保存截图;禁止跨状态复用旧计数。断线立即取消输入、释放目标和待处理操作。统一 teardown 断开监听,场景 free 后 probe 不再访问节点。
### 6.3 用例与完成条件
- NET-01:完整闭环 3 次,每次独立进程与报告。
- NET-02:无效账号和不存在槽位,明确错误且有界退出(限制尝试,避免账号锁定)。
- NET-03:进场期间断线,不能留下半加载操作对象。
- NET-04:目标在攻击前消失,取消操作而非继续攻击旧 VID。
- NET-05:掉落被他人拾取、背包已满,不能误报成功;需专用夹具或离线负测,实网缺条件须标出。
自动调用业务入口不等于鼠标命中正确;另做一次真实鼠标/键盘闭环,记录输入设备与截图,作为手动必测项。
## 7. CBT-01:基础战斗和技能
新增 `project/playable_combat_test.gd`(离线事件与输入断言)、`test/playable/skill-cases.json`(实际角色技能矩阵)。联网用例由 playable_live_test 调度,不再创建第二条发包路径。
### 7.1 实现与定位
1. 通过 Quickbar.assign/restore_from_server 构造测试账号实际槽位;需要持久化变更时仅对专用账号执行,记录原值并在可行时恢复。无服务端技能等级不得本地伪造。
2. 调用 activate 复用 PlayerSkill.use_skill,监听现有 skill_cast_started(skill_id,target_vid) 与 skill_rejected(skill_id,code)。cast_started 只是本地施法开始,不是服务端命中证明。
3. case_id 绑定施法时目标;施法中换目标后,定点效果仍使用原目标快照,跟随效果按技能定义更新。
4. probe 关联接收端资源/状态变化、目标血量或伤害事件;不同技能配置 required_evidence,不强制所有技能都有伤害或资源消耗。
5.`game_scene.gd``fx/skill_fx.gd``fx/effect_player.gd` 的实际生成/结束边界记录测试事件。不可只用节点数量增加判定特效正确。
6. 自动截图覆盖施法前、触发帧、峰值、结束;短命特效按事件时间采样,不统一等一秒。
诊断路由:图标/点击区域→quickbar;拒绝条件/目标→player_skill;动作触发→net_play 与玩家视图;挂点/生命周期→game_scene、skill_fx、effect_player;骨骼坐标→metin2_model/metin2_anim。只在复现后修改对应层。
### 7.2 测试矩阵
每职业从真实已学技能中选择适用类型:单体、范围、自身增益、飞行效果;不存在的类型标注不适用并经清单确认,不擅自补技能 ID。每技能成功施放 10 次;另覆盖无目标、超距、冷却、资源不足、目标死亡、自身死亡、切图/断线、目标切换。
断言包括:拒绝不发有效施法请求;正常请求数无重复;服务端效果可观察;角色持续可见且包围盒有限;特效在定义寿命+1秒宽限内清理;挂点相对预期骨骼/目标坐标误差默认不超过 0.1m(有资源偏移需记录校准值)。
`skill_test.gd``player_skill_test.gd``combat_fx_test.gd``target_effect_test.gd` 扩展相应失败用例,并纳入 rendering_batch_test。截图必须人工复核一次,像素非空不能证明技能外观正确。
完成条件:必测职业/技能覆盖完整、功能阻断清零、首次施法与重复施法均通过;高级特效差异写入已知问题,不改成假 PASS。
## 8. MAP-01/02:鬼木林、赤鬼木林与树怪
### 8.1 资源审计
新增 `script/audit_playable_maps.mjs`,输出 `map-assets.json`。读取实际 map 配置、AreaData/Property、npclist、MSM、motlist 和引用文件,分别列出静态 SPT、动态 GR2、贴图、动作及缺失项。没有服务端刷新数据时只报告候选树怪,不声称已确定地图所有怪物。
已核实 `assets/root/npclist.txt`23012307 对应 ent_trent/ent_guru/ent_hu/ent_red/ent_black/ent_huge/ent_elder23112315 对应前五种。共享模型仍保留独立 race 用例,避免漏掉 proto/外观配置差异。实际 map key 与刷新关联由前置审计填入配置。
### 8.2 单模型 GPU 验收
新增 `project/forest_mob_render_test.gd`,沿用 MobView.build、set_anim_state 和现有 Metin2Model/Metin2AnimPlayer。读取 motlist 的具体文件,报告 requested_state、resolved_motion、fallback_reason;“调用 dead 却播放 wait”不能通过。
对 12 个候选 race × CPU/GPU × wait/run/attack/damage/dead 生成基准集合;资源确无动作时据旧客户端回退规则单独判定,不能静默跳过。动作每个采样起点、25%、50%、75%、末段;固定随机种子或明确选择变体。死亡动作不能为了截图强行循环。
自动断言:构建成功;贴图实际加载;顶点/包围盒有限;无非法材质;同一姿态 CPU/GPU 包围盒一致(初始容差 1cm 或高度0.5%,取较大值);截图非空。容差变更需记录理由,不通过放宽到无限解决失败。
输出每 race 的正面、侧面、动作 contact sheet 和 JSON。检查材质双面/裁剪应依资源,不对所有树怪强制双面;蒙皮异常先检查骨骼索引、权重、bind pose、坐标,再修改 C++ 公共路径。`mob_winding_test.gd` 增加这些 race,若影响全怪物必须回归原 101/110/20001。
### 8.3 地图内验收
新增 `project/forest_map_render_test.gd` 和包内 `MT_TEST_MODE=forest_render` 分支,加载配置指定的生产地图。新测试模式不存在于旧包,必须重新构建后执行。
在经确认的平地、坡地、密林和传送点保存固定机位截图,记录 world.sample_height、怪物世界位置、ground offset、材质和实际动作路径。平地不允许持续悬空/埋地超过校准阈值;坡地需人工检查脚/根部接触,AABB 底部不是精确足部,不能用它宣布足部 IK 完成。
实网追加逐地图进入、选中树怪、攻击、死亡、拾取、离开。静态地图截图与动态服务器刷新分别记录证据,不混为一次测试。
完成条件:首测区域关键资源无未解释缺失、主要动作和目标交互正常、Metal 截图人工签核;其余地图不在本轮覆盖中。
## 9. STB-01:稳定性与长帧
新增 `project/testing/playable_metrics.gd``script/playable_soak.sh`,扩展 rendering_scenario_test 的 P99 与事件时间点,但保留“离线合成”和“真实联网”两种 suite 标签。
### 9.1 指标实现
- 每帧记录 monotonic 间隔;按 map/phase/actor_count/gpu_skin/动作切换标记。加载与游玩分段统计。
- 固定大小环形缓冲,按时间窗口输出分位数和 >50ms/>100ms 样本;不得无限数组增长制造测试自身内存泄漏。
- 默认关闭逐帧打印,只保留长帧事件;采样开启/关闭各跑短基线,量化探针开销。
- 外部采样父运行器所持 PID 的 RSS,每秒一次,记录单位 KiBGodot MEMORY_STATIC 单独记录。GPU 内存拿不到写 null/unavailable,不填写 0 或据此宣称无显存泄漏。
- 缓存预热后比较重复路线每轮结束并静置30秒的低水位;记录前5轮与后5轮中位数。连续5轮增长且后5轮中位数高出 max(50MiB,5%) 时门禁失败并分析;阈值以下也不能证明长期无泄漏。
### 9.2 执行场景与边界
2小时实网循环移动/战斗/拾取;20次经合法传送入口切图;10次重连;3种受支持逻辑分辨率各切10次;10次正常退出。窗口物理像素与逻辑尺寸都记入报告,不能把 Retina 缩放当成分辨率错误。
主动 reconnect() 与真实传输故障分别计数。真实故障至少覆盖“连接被关闭”和“短时不可达”;仅测试专用连接,可使用经确认的局部代理/测试网络条件,不改全机路由、防火墙、不关生产服务。缺少该条件标为 BLOCKED,不用主动重连替代断网验收。
时间采用墙钟期限,不用固定帧数冒充2小时。重连后重新获取主 VID、场景和 UI 对象,检测重复信号、重复实体、旧目标/技能残留。跨服切图需核实测试服务器支持,失败不能通过直接传送客户端节点掩盖。
### 9.3 长帧处理流程
先标记同步模型 build、动作 reload、GR2解析、材质初次使用和实体批量生成;记录缓存命中。确认瓶颈后才做路径级修改:动画缓存→metin2_anim;实例建立→net_world/game_scene;材质预热→实际渲染建立路径。后台工作只做线程安全的解析,不把 SceneTree/RenderingServer 操作随意搬入工作线程。
相同机器/路线对比修改前后3轮,保留最大帧而非只报平均。正常游玩目标60FPS;同一交互3次重复中至少2次出现 >100ms 视为阻断,任何 >50ms 样本需归因。首次加载独立预算在基线采集后固定,不临时扩大预算让门禁通过。
完成条件:零崩溃/永久卡死、循环后功能正常、无达到上述阈值的未解释内存增长、无重复严重长帧、每次退出干净。冷启动报告注明进程冷启动/系统缓存状态未知,不执行系统缓存清空来影响用户其他程序。
## 10. 执行命令与验证顺序
以下命令在仓库根目录执行。账号通过安全交互提供,禁止放入命令历史。新脚本落地前不得宣称这些未来命令已可运行。
现有可执行回归:
```bash
bash script/rendering_batch_test.sh
MT_RENDER_APP="$PWD/build/export-native-trees-20260911/mtgodot-poc.app" bash script/package_render_test.sh
```
待实现 CLI 契约(各脚本必须有 --help、配置校验、非零失败码):
```bash
# 离线资源/蒙皮/报告门禁,不连接服务器
node script/audit_playable_maps.mjs --config test/playable/scenario.local.json --output build/playable/map-assets.json
godot --headless --path project --script playable_harness_test.gd
godot --path project --script forest_mob_render_test.gd
# 重新构建带新包内测试入口的候选包,不能用旧包冒充
MT_MAC_ARCHES=arm64 MT_MAC_ENGINE="$PWD/build/godot-particle-diagnostic/bin/godot.macos.template_release.arm64" MT_MAC_OUTPUT_DIR="$PWD/build/export-playable-rc1" bash build-macos-client.sh release
# 明确显式允许专用账号上的游戏操作;默认仅校验配置,不发移动/战斗请求
bash script/playable_test.sh --app "$PWD/build/export-playable-rc1/mtgodot-poc.app" --config test/playable/scenario.local.json --suite full --allow-gameplay --repeat 3
bash script/playable_soak.sh --app "$PWD/build/export-playable-rc1/mtgodot-poc.app" --config test/playable/scenario.local.json --allow-gameplay --duration-seconds 7200
node script/validate_playable_report.mjs --release-dir build/playable/rc1
```
新脚本参数还需支持 --output;聚合器只接受显式列出的本次 run_id 与相同包哈希,不扫描历史 PASS 凑覆盖。测试模式、测试账号均禁止自动聊天、交易、出售、丢弃物品或创建/删除角色;不沿用旧 smoke 的聊天副作用。
配置 local.json、凭据文件、运行产物加入适当忽略规则;提交无敏感信息的 example/schema。最终修复 C++ 后必须重建同步扩展、重新签名和打包;脚本变更也必须重新导出 PCK。
## 11. REL-01:放行检查表
- [ ] INF-01/02 负向用例通过,必测项缺失不能 PASS。
- [ ] 同一候选包3轮联网闭环通过,另有鼠标键盘流程证据。
- [ ] 四职业基础战斗、技能及异常路径清单完整。
- [ ] 首测地图和树怪资源/动作/Metal画面验收通过。
- [ ] 2小时、切图、故障恢复、窗口缩放、退出门禁达到配置次数。
- [ ] P0(崩溃/数据异常/无法进场)与 P1(核心操作失效/主要模型丢失/严重错位/重复严重卡顿)清零。
- [ ] 所有 BLOCKED 明确处理;首测必测项不得以“已知问题”绕过。
- [ ] 包哈希、签名、引擎来源、设备信息与报告一致。
- [ ] 已知 P2、未覆盖地图/装备/技能及未验收平台明确列出。
官方 Release 模板仍有粒子退出警告;必须显式使用验证过的自编译 arm64 引擎,并对最终包重新跑退出门禁。不将当前方案表述为已修复所有官方模板或 Intel 平台。
完成文档不等于完成上述开发。开发起点是 INF-01/02;它们完成后 NET-01 与 MAP-01 可并行推进。工期在首次真实闭环和树怪截图产生后根据实际缺陷估算。