Files
shenleiandClaude Opus 5 f39a55fdd5 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
2026-09-11 22:06:48 +09:00

66 lines
3.0 KiB
GDScript

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