feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档

- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay
  只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续
  运行并记 BLOCKED;写本批次 release-manifest.json 后聚合
- run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理);
  soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数)
- 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试
- forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别
- 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求
- .gitignore 排除本地场景配置、凭据文件与运行输出

离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS,
node 夹具测试 PASS。未联网运行,未重建候选包。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
This commit is contained in:
shenlei
2026-09-11 22:06:48 +09:00
co-authored by Claude Opus 5
parent 374d4165d8
commit f39a55fdd5
66 changed files with 9189 additions and 951 deletions
+35
View File
@@ -124,3 +124,38 @@ func _run() -> void:
await process_frame
_ck(nw.node_for(2) != null, "main back in range -> culled entity rebuilt after its fade")
_ck(int(added.get(2, 0)) == 2, "entity 2 rebuilt exactly once on re-entry")
await _dead_motion(fc, nw)
class AnimView extends Node3D:
var states: Array[String] = []
func set_anim_state(s: String) -> void:
states.append(s)
# MAP-02 / CActorInstance::Die: a model plays NAME_DEAD only (no scene tilt), and
# __SetMotion rejects every non-dead motion while the actor is dead — the
# func/moving edge that arrives with the death must not restart WAIT.
func _dead_motion(fc: FakeClient, nw: Node) -> void:
nw.set_model_factory(func(d: Dictionary) -> Node3D:
return AnimView.new() if int(d.get("race", 0)) == 2301 else null)
var ent := fc.add(6, NEAR_CM + Vector2(300, 0), {"race": 2301, "ch_type": 2, "moving": true})
fc.entity_spawned.emit(ent)
await process_frame
var view := nw.node_for(6) as AnimView
_ck(view != null and view.states.back() == "run", "moving tree monster -> run requested")
if view == null:
return
ent["moving"] = false
ent["dead"] = true
fc.entity_dead.emit(6)
await create_tween().tween_interval(0.4).finished
await process_frame
_ck(view.states.back() == "dead", "death while moving keeps DEAD (reference __SetMotion ignores WAIT when dead): %s" % [view.states])
_ck(is_zero_approx(view.rotation.x) and is_zero_approx(view.rotation.z), "model with motions is not tilted on death (rotation.x=%.2f)" % view.rotation.x)
var capsule := fc.add(7, NEAR_CM + Vector2(600, 0), {"ch_type": 2})
fc.entity_spawned.emit(capsule)
var placeholder: Node3D = nw.node_for(7)
capsule["dead"] = true
fc.entity_dead.emit(7)
await create_tween().tween_interval(0.4).finished
_ck(placeholder != null and placeholder.rotation.x < -1.0, "placeholder capsule (no motions) still falls over as its only death cue")