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

23 KiB
Raw Blame History

首个 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 必须具有如下字段:

{
  "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.shscript/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.gdfx/skill_fx.gdfx/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.gdplayer_skill_test.gdcombat_fx_test.gdtarget_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.txt23012307 对应 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.gdscript/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 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、配置校验、非零失败码):

# 离线资源/蒙皮/报告门禁,不连接服务器
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 可并行推进。工期在首次真实闭环和树怪截图产生后根据实际缺陷估算。