#!/usr/bin/env bash # 渲染闸正主——窗口化自动闸。 # 范围(渲染五闸的机器面:shot_probe/golden_compare/board_shot 三件入闸; # fx_probe 逻辑层归 probe_gate --full;1920 档布局机检面待补)。 # 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 # Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。 # 段: # ① shot_probe 七屏窗口化截图(menu/settings/career/camp/level/battle/status) # → tools/shot_checker.py A-D 语义断言自动判定(A 屏级骨架全屏/B 文字对比度/C 明度三档带/ # D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记待办清单。 # ② 取证段(Q7:超时/零字节/文件缺失任一即 FAIL——「渲染真实发生」必须成立,不做像素判定): # fx_shot_probe 三时刻抓帧(行到达/结算排播/轮末连帧)+ board_shot_probe L1-L3 棋盘抓帧。 # ③ 棋盘 golden 逐像素:board 整屏帧按 board_meta.json 取样坐标裁剪棋盘区 # (tools/golden_board_crop.py——禁硬编码像素)→ tools/golden_compare.py 对 # game/tests/golden/ 三张基线(容差 provisional:差 ≤16/changed ≤2%,S2 停机点口径)。 # 素材缺位=判红:缺盘时这一段根本不对比,若 SKIP 则闸末照样打 # 「全绿」exit 0,等于空跑判绿。基线生成期素材缺位=L3 停机,运行期同样无权出验收结论。 # ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比 # 1.35×+左右黑边各 96px,构图与基准档一致(分辨率案甲)——分辨率维度不重复建闸。 # (1920 布局机检面现无承载,待补。) # ⑤ 脚本错误扫描(三硬闸之二):shot/fx/board 三段 Godot 日志逐段过 tools/error_scan.py, # 命中 SCRIPT ERROR/Parse Error 即红(标记与 run_smoke.py 同源);python 工具自身输出不扫。 # 此类错误不改退出码、不计失败数,且会中断所在函数的剩余断言——只能由外部扫日志。 # 新增日志段必须同步加扫描调用,漏加=该段静默错误漏网。 # 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git; # 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。 # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸 # (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。 # 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree) # 依赖: tools/gate_platform.sh(双平台封装)+ python(PIL);Windows 侧另用 taskkill/PowerShell,macOS/Linux 走共享件;Godot 路径可用环境变量 GODOT_BIN 覆盖。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" . "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀) GAME="${1:-$REPO/game}" GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样) GOLDEN="$REPO/game/tests/golden/$(gate_platform_id)" # 本平台基线目录(按平台各存,比较器同口径) TMP="$(mktemp -d /tmp/rendergate_XXXXXX)" trap 'rm -rf "$TMP"' EXIT _rc=0 orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台) gate_orphans "$GAME_WIN" } kill_orphans() { gate_kill_orphans "$GAME_WIN" } scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红 local name="$1" log="$2" if ! python3 "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):" sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /' _rc=1 fi } # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 kill_orphans fi pre="$(orphans | wc -l | tr -d ' ')" if [ "$pre" -gt 0 ]; then echo "[render_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi echo "======== shot_probe 七屏截图 → shot_checker A-D ========" SHOT="$TMP/shots" gate_run_timeout 360 "$TMP/shot.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/shot_probe.tscn -- "$SHOT" st=$?; kill_orphans grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12 # shot 判定锚工件:问题=0 签名在位=完成(124 为 macOS 退出段挂死回收/quit-after 竞速,非真红——run21/26 实证) if ! grep -q "问题=0" "$TMP/shot.log"; then echo "[render_gate] FAIL — shot_probe 未干净完成(exit=${st},缺失屏/空文件/路径断裂见上)"; _rc=1 fi if [ "$_rc" -eq 0 ]; then python3 tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /' _rc="${PIPESTATUS[0]}" [ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)" fi scan_log "shot_probe" "$TMP/shot.log" echo "======== fx_shot_probe 三时刻取证(Q7) ========" FX="$TMP/fxshots" gate_run_timeout 360 "$TMP/fx.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/fx_shot_probe.tscn -- "$FX" st=$?; kill_orphans grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4 # fx 判定锚工件:取证双检(fx_evidence)+ 错误扫描(scan_log)已覆盖真红——macOS 下 # quit-after 与 quit(0) 竞速的 124 退出非真红(run21 实证),不单独判闸。 scan_log "fx_shot_probe" "$TMP/fx.log" fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7) local pat="$1" min="$2" n=0 f for f in "$FX"/$pat; do [ -f "$f" ] || continue [ -s "$f" ] || { echo "[render_gate] FAIL — 取证零字节: $f"; _rc=1; return; } n=$((n+1)) done if [ "$n" -lt "$min" ]; then echo "[render_gate] FAIL — 取证文件缺失: $pat 仅 $n/<$min"; _rc=1; fi } fx_evidence "fx_dig*.png" 1 fx_evidence "fx_settle*.png" 1 fx_evidence "fx_round*.png" 6 fx_evidence "fx_final.png" 1 echo "======== board_shot_probe L1-L3 取证(Q7) ========" BD="$TMP/boardshots" gate_run_timeout 360 "$TMP/bd.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/board_shot_probe.tscn -- "$BD" st=$?; kill_orphans grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6 # board 判定锚工件:board_meta.json 非零 + 错误扫描覆盖真红;124 竞速退出非真红(同 fx)。 scan_log "board_shot_probe" "$TMP/bd.log" for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi done echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========" if [ ! -d "$GOLDEN" ]; then echo "[render_gate] FAIL — 本平台($(gate_platform_id))基线目录缺失:${GOLDEN}(基线按平台各存、在库=验收断言②)"; _rc=1 elif [ ! -s "$BD/board_meta.json" ]; then echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判" else # 素材缺位=判红:缺盘态下这一段一个像素都不比,而闸末照样打印 # 「全绿」退出码 0——那是空跑判绿,比误红危险得多。基线锚在素材齐盘态(Q5),缺盘态本就无权出验收结论。 _asset_miss=0 for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/board/seal_mark.png \ assets/art/character/character_hero_idle.png \ assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \ assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \ assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do [ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: ${a}(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; } done if [ "$_asset_miss" -eq 0 ]; then CROP="$TMP/crop" python3 tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 棋盘区裁剪失败"; _rc=1 else python3 tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \ board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — golden 逐像素超容差(S2 停机点:字体/平台误红呈报后定容差)"; _rc=1 fi fi fi fi echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========" if [ ! -s "$GOLDEN/menu_boot.png" ]; then echo "[render_gate] FAIL — $GOLDEN/menu_boot.png 基线缺失(菜单 golden 未建——本平台跑 tools/golden_baseline_refresh.sh --menu)"; _rc=1 elif [ ! -s "$SHOT/shot_menu.png" ]; then echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判" elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then echo "[render_gate] FAIL — 菜单素材缺位(字体/按钮石纹):基线锚素材齐盘态,缺盘不得出验收结论(2026-10-17 由 SKIP 改判红)"; _rc=1 else MENUG="$TMP/menugolden" mkdir -p "$MENUG" cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png" python3 tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1 fi fi echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" total="$(gate_godot_count_all)" echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)" if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi exit "$_rc"