From 8bfd2494021b032bf71e3232aaa397530d67cc23 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 27 Oct 2026 05:23:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(user=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E9=9A=94=E7=A6=BB):=20=E6=AF=8F=E4=B8=AA=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=9D=A2=20worktree=20=E7=8B=AC=E7=AB=8B=20Godot=20user=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=EF=BC=8B=E7=B3=BB=E7=BB=9F=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=20V3d=20=E7=BC=BA=E5=B8=A7=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 机制: tools/setup_worktree_userdir.sh 在建区后生成 game/override.cfg(use_custom_user_dir + 唯一名 DungeonSweeper_<区名>_<绝对路径 sha1 前 8>), 主工作区 no-op 保持真实档原位; .gitignore 收 game/override.cfg - 闸壳: run_all_gates.sh 参数解析后先自检/自愈本区隔离, 建不起来 exit 1(fail-closed)——并行批共用一套夹具/日志/截图目录会互相踩盘, 是假红的根源 - 读取面: 新增 tools/godot_user_dir.py 统一解析(有 override→custom dir, 无→默认 appdata/Godot/app_userdata/); parity_anchor.py 与 analyze_playtest.py 的硬编码 APPDATA 路径改为调用它 - 流程: process-conventions §1 第 1 条(建区即跑 setup)与 §3 环境基线第 4 条(闸壳自检); iter 技能冷检出三件套→四件套(.zcode/.qoder 两份本机副本已同步, 不入 git) - 系统测试 V3d: 隔离后空 user 目录(无 settings.cfg)使 reduced_motion 取默认 false, 实查暴露 V3d 真实缺陷——测试同步驱动整场战斗后同帧 flush, RowPacer._was_pumping 从未置真, 演播后排窗的 pump_ended 边沿不触发, _end_overlay 永不打开; 旧绿只因本机 settings.cfg reduced=true 让排播走 consume_now 快路绕过队列。补一帧 await process_frame 让泵先观测到非空队列再 flush, 恢复真实边沿路径 - 取证: 隔离空目录下 00 冒烟 17/17、02 单元与集成 62 套全绿、03 探针 --click 全绿; 01 系统测试修正前 70/1 红(仅 V3d)、修正后 reduced=true 与 false 两档均 70/70 绿 --- .gitignore | 3 ++ design/engineering/process-conventions.md | 4 +- game/tests/system_test.gd | 1 + tools/analyze_playtest.py | 6 ++- tools/godot_user_dir.py | 26 +++++++++++++ tools/parity_anchor.py | 5 ++- tools/run_all_gates.sh | 8 ++++ tools/setup_worktree_userdir.sh | 45 +++++++++++++++++++++++ 8 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 tools/godot_user_dir.py create mode 100644 tools/setup_worktree_userdir.sh diff --git a/.gitignore b/.gitignore index cbbe51f1..ef6a5961 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ # 导入元数据 *.import +# 代码面 worktree 的 Godot user 目录隔离件(tools/setup_worktree_userdir.sh 生成;主区不生成) +game/override.cfg + # 导出构建产物 builds/ exports/ diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index fae1a3b2..06bb9523 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -9,7 +9,7 @@ 改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一): -1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运)。迭代批开工细则见 §3 与 iter 技能 +1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运);建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。迭代批开工细则见 §3 与 iter 技能 2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑),然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`,再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑 3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制 4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent @@ -40,7 +40,7 @@ 3. **用户发开工令才动手**——需求卡片经用户批准(U0)后方开工 **环境与基线** -4. **代码面批独立 worktree+拷导入缓存**——触及代码面的批次按 §1 建 worktree;先拷主区 `.godot/` 目录与字体 `.import` 边车再跑测试,否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);直改车道(非代码面)无此项 +4. **代码面批独立 worktree+拷导入缓存**——触及代码面的批次按 §1 建 worktree;先拷主区 `.godot/` 目录与字体 `.import` 边车再跑测试,否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);闸壳每次跑批先自检/自愈本区 user 目录隔离(§1 第 1 条),建不起来即拒跑;直改车道(非代码面)无此项 5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟 **实施** diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 11b0f568..e61fd894 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -466,6 +466,7 @@ func _smoke_lifecycle_matrix() -> void: guard += 1 check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(), "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)]) + await process_frame main._flush_row_queue() for i in range(12): if main._end_overlay.is_open(): diff --git a/tools/analyze_playtest.py b/tools/analyze_playtest.py index 94752ed0..524c8e47 100644 --- a/tools/analyze_playtest.py +++ b/tools/analyze_playtest.py @@ -30,6 +30,9 @@ import glob import json import os import sys +from pathlib import Path + +from godot_user_dir import resolve_user_dir # ---- 表值区(溯源见文件头) ---- POOL_CLAMP = (-100, 100) @@ -613,8 +616,7 @@ def print_report(label, per_battle, t, exact_mode): def main(): - log_dir = os.path.join(os.environ.get("APPDATA", ""), "Godot", "app_userdata", - "DungeonSweeperVerticalSlice", "logs") + log_dir = str(resolve_user_dir(Path(__file__).resolve().parent.parent) / "logs") path = None args = sys.argv[1:] if args and not args[0].startswith("-"): diff --git a/tools/godot_user_dir.py b/tools/godot_user_dir.py new file mode 100644 index 00000000..f2833788 --- /dev/null +++ b/tools/godot_user_dir.py @@ -0,0 +1,26 @@ +"""解析 Godot 的 user:// 根目录——与 game/override.cfg 的隔离口径同一份。 + +无 override.cfg(主工作区)→ %APPDATA%/Godot/app_userdata/; +有 override.cfg(代码面 worktree,由 tools/setup_worktree_userdir.sh 生成)→ %APPDATA%/。 +""" +import os +import re +from pathlib import Path + + +def _project_name(repo_root: Path) -> str: + text = (Path(repo_root) / "game" / "project.godot").read_text(encoding="utf-8", errors="replace") + m = re.search(r'^config/name="([^"]*)"', text, re.M) + return m.group(1) if m else "DungeonSweeperVerticalSlice" + + +def resolve_user_dir(repo_root: Path) -> Path: + base = Path(os.environ.get("APPDATA") or (Path.home() / "AppData" / "Roaming")) + override = Path(repo_root) / "game" / "override.cfg" + if override.is_file(): + text = override.read_text(encoding="utf-8", errors="replace") + custom = re.search(r'^config/use_custom_user_dir=true\s*$', text, re.M) + name = re.search(r'^config/custom_user_dir_name="([^"]*)"', text, re.M) + if custom and name and name.group(1): + return base / name.group(1) + return base / "Godot" / "app_userdata" / _project_name(Path(repo_root)) diff --git a/tools/parity_anchor.py b/tools/parity_anchor.py index 80948c61..122bf360 100644 --- a/tools/parity_anchor.py +++ b/tools/parity_anchor.py @@ -24,18 +24,19 @@ import argparse import hashlib import json -import os import re import subprocess import sys import time from pathlib import Path +from godot_user_dir import resolve_user_dir + ROOT = Path(__file__).resolve().parent.parent GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" GAME_DIR = ROOT / "game" ANCHOR_PATH = ROOT / "game" / "tests" / "evidence" / "parity" / "anchor.json" -USER_LOGS = Path(os.environ.get("APPDATA", "")) / "Godot" / "app_userdata" / "DungeonSweeperVerticalSlice" / "logs" +USER_LOGS = resolve_user_dir(ROOT) / "logs" # 变体表:名字 → (seed0, attune, shop)。(N=20/策略 both/route min/feat off 全变体钉死。) VARIANTS = { diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 129af25a..9171e07c 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -41,6 +41,7 @@ GAME="$REPO/game" STAMP_DIR="$GAME/tests/.gates" RECEIPT="$STAMP_DIR/receipt.json" BASELINE="$STAMP_DIR/baseline.json" + QUICK=0 DOMAIN="full" for _a in "$@"; do @@ -53,6 +54,13 @@ for _a in "$@"; do esac done +# 每个代码面 worktree 用独立 Godot user 目录(game/override.cfg 由 setup 脚本自愈生成;主区 no-op)。 +# 并行批共用一套夹具、日志与截图目录会互相踩盘造成假红——隔离建不起来就拒跑(fail-closed)。 +"$REPO/tools/setup_worktree_userdir.sh" "$REPO" || { + echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。" + exit 1 +} + # 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛 # UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移, # 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。 diff --git a/tools/setup_worktree_userdir.sh b/tools/setup_worktree_userdir.sh new file mode 100644 index 00000000..58d5ef44 --- /dev/null +++ b/tools/setup_worktree_userdir.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# 代码面 worktree 的 Godot user 目录隔离:给每个 worktree 生成 game/override.cfg, +# 让该区的 user:// 落到独立目录(夹具、日志、截图产物不再与其他区共用)。 +# 主工作区不生成——真实存档与试玩进度保持原位。 +# +# 用法:tools/setup_worktree_userdir.sh [仓库根](缺省=本脚本所在仓库) +# 幂等:内容一致即不重写;退出码 0=已隔离或主区免隔离,1=生成失败(闸壳据此拒跑) +set -u +ROOT="$(cd "${1:-$(dirname "$0")/..}" && pwd)" + +MAIN_WT="$(git -C "$ROOT" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)" +if [ -z "$MAIN_WT" ]; then + echo "[userdir] 取不到主工作区路径(git worktree list 为空)" >&2 + exit 1 +fi +if [ "$(cd "$ROOT" && pwd -P)" = "$(cd "$MAIN_WT" && pwd -P)" ]; then + echo "[userdir] 主工作区:不隔离(真实档保持原位)" + exit 0 +fi + +BASE="$(basename "$ROOT" | tr -d '\n' | tr -c 'A-Za-z0-9_-' '_')" +ABS="$(cd "$ROOT" && pwd -P)" +if command -v sha1sum >/dev/null 2>&1; then + FP="$(printf '%s' "$ABS" | sha1sum | cut -c1-8)" +else + FP="$(printf '%s' "$ABS" | cksum | tr -d ' ' | cut -c1-8)" +fi +NAME="DungeonSweeper_${BASE}_${FP}" +CFG="$ROOT/game/override.cfg" + +CONTENT="[application] + +config/use_custom_user_dir=true +config/custom_user_dir_name=\"$NAME\" +" +if [ -f "$CFG" ] && printf '%s' "$CONTENT" | cmp -s - "$CFG"; then + echo "[userdir] 已隔离:$NAME" + exit 0 +fi +mkdir -p "$ROOT/game" 2>/dev/null +if ! printf '%s' "$CONTENT" >"$CFG"; then + echo "[userdir] 写 $CFG 失败" >&2 + exit 1 +fi +echo "[userdir] 已隔离:$NAME"