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:
@@ -0,0 +1,65 @@
|
||||
extends RefCounted
|
||||
|
||||
## STB-RESIZE-01 的真实窗口适配器(PlayableFlow.window)。
|
||||
##
|
||||
## macOS Retina 下 DisplayServer 的窗口尺寸是物理像素,逻辑尺寸 = 物理像素 / 屏幕缩放。
|
||||
## 流程只按逻辑尺寸判定;物理像素、缩放和视口尺寸一并写进事件,Retina 的 2x 不是分辨率错误。
|
||||
## 请求尺寸超出当前屏幕可用区域(含标题栏)时如实拒绝(request_logical_size 返回 false),
|
||||
## 由流程记为 BLOCKED,不缩小目标冒充通过。headless / 全屏 / 最大化窗口不可调整。
|
||||
|
||||
var _viewport: Viewport
|
||||
|
||||
func _init(viewport: Viewport = null) -> void:
|
||||
_viewport = viewport
|
||||
|
||||
func availability() -> Dictionary:
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return {"ok": false, "reason": "headless display server has no window"}
|
||||
match DisplayServer.window_get_mode():
|
||||
DisplayServer.WINDOW_MODE_FULLSCREEN, DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN:
|
||||
return {"ok": false, "reason": "window is fullscreen; logical size switching needs windowed mode"}
|
||||
DisplayServer.WINDOW_MODE_MAXIMIZED:
|
||||
return {"ok": false, "reason": "window is maximized; logical size switching needs windowed mode"}
|
||||
DisplayServer.WINDOW_MODE_MINIMIZED:
|
||||
return {"ok": false, "reason": "window is minimized"}
|
||||
if screen_scale() <= 0.0:
|
||||
return {"ok": false, "reason": "screen scale unavailable"}
|
||||
return {"ok": true, "reason": ""}
|
||||
|
||||
func screen_scale() -> float:
|
||||
return DisplayServer.screen_get_scale(DisplayServer.window_get_current_screen())
|
||||
|
||||
func physical_size() -> Vector2i:
|
||||
return DisplayServer.window_get_size()
|
||||
|
||||
func logical_size() -> Vector2i:
|
||||
var scale := screen_scale()
|
||||
var size := physical_size()
|
||||
if scale <= 0.0:
|
||||
return size
|
||||
return Vector2i(roundi(size.x / scale), roundi(size.y / scale))
|
||||
|
||||
func viewport_size() -> Vector2i:
|
||||
if _viewport == null or not is_instance_valid(_viewport):
|
||||
return Vector2i.ZERO
|
||||
return Vector2i(_viewport.get_visible_rect().size)
|
||||
|
||||
## Returns false (no request sent) when the size cannot fit the usable screen area.
|
||||
func request_logical_size(size: Vector2i) -> bool:
|
||||
var scale := screen_scale()
|
||||
if scale <= 0.0 or size.x <= 0 or size.y <= 0:
|
||||
return false
|
||||
var target := Vector2i(roundi(size.x * scale), roundi(size.y * scale))
|
||||
var screen := DisplayServer.window_get_current_screen()
|
||||
var usable := DisplayServer.screen_get_usable_rect(screen)
|
||||
var decorations := DisplayServer.window_get_size_with_decorations() - DisplayServer.window_get_size()
|
||||
if target.x + decorations.x > usable.size.x or target.y + decorations.y > usable.size.y:
|
||||
return false
|
||||
DisplayServer.window_set_size(target)
|
||||
# Keep the whole window on the usable area so the OS does not clamp the size back.
|
||||
var position := DisplayServer.window_get_position()
|
||||
var clamped := Vector2i(clampi(position.x, usable.position.x, usable.end.x - target.x - decorations.x),
|
||||
clampi(position.y, usable.position.y, usable.end.y - target.y - decorations.y))
|
||||
if clamped != position:
|
||||
DisplayServer.window_set_position(clamped)
|
||||
return true
|
||||
Reference in New Issue
Block a user