Merge branch 'master' into iter40

This commit is contained in:
DKChan committed 2026-10-27 08:00:37 +08:00
commit 3a20a40891
12 files changed
+127 -38

No files matched your search

+3
View File
@@ -7,6 +7,9 @@
# 导入元数据
*.import
# 代码面 worktree 的 Godot user 目录隔离件(tools/setup_worktree_userdir.sh 生成;主区不生成)
game/override.cfg
# 导出构建产物
builds/
exports/
+2 -2
View File
@@ -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. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟
**实施**
+2 -2
View File
@@ -1,5 +1,5 @@
{
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿",
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿",
"gates": {
"00 冒烟测试": {
"exit": "0",
@@ -47,7 +47,7 @@
},
"09 静态扫描": {
"exit": "1",
"fp": "user-approved-iter38"
"fp": "91d45583cf6d"
},
"10 熵增闸": {
"exit": "0",
+4 -2
View File
@@ -291,8 +291,10 @@ fi
# probe/sim/tests 测量面直构惯例不在面)。存量基线 tools/gdstyle_baseline.txt 用稳定身份键(函数级带数
# 条目=文件|规则|函数名,类级/文件级带数条目=文件|规则,均带登记数值;无数字条目=文件|规则|消息全文
# 裸键)——消息里的行数/分支数等计数漂移不假红;判定=基线外新违规红(NEW)+同键指标较登记值恶化红
# (WORSENED),改小/持平不红。基线重生成=人工专用(python tools/gdstyle_gate.py --refresh,附对账;
# 键数骤降逾 10% 须 --force),禁清基线换绿。二进制本机固定路径
# (WORSENED),改小/持平不红;逐条违规行带 FAIL 前缀,使存量红期间的红项清单(哪几条、恶化到多少)
# 进 gate 09 指纹,新增或进一步恶化不会被「同款存量红」放过。阈值口径=gdstyle 默认值;基线只是欠账
# 簿,登记值只准随还债下降,禁抬高到现状。基线重生成=人工专用(python tools/gdstyle_gate.py
# --refresh,附对账;键数骤降逾 10% 或登记值被抬高均拒写,须 --force),禁清基线换绿。二进制本机固定路径
# D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。
if python tools/gdstyle_gate.py; then
echo "[static_scan] S13 ok"
+1
View File
@@ -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():
+4 -2
View File
@@ -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("-"):
+3 -19
View File
@@ -3,19 +3,13 @@
# (class variables/public methods/file length)=文件|规则;无数字条目=文件|规则|消息全文(裸键)。
# 带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键。
# 判定=键不在基线红(NEW)、同键数值较登记值恶化红(WORSENED)、改小/持平不红。
# 阈值口径=gdstyle 默认值(tools/gdstyle.toml 不设数值阈值);本文件只是欠账簿,登记值只准随
# 还债下降,禁抬高到现状(抬高=把既有代码当尺子,默认恶化)。
# 重生成=人工专用:python tools/gdstyle_gate.py --refresh(禁清基线换绿;改规则口径/工具升级须随批
# 重生成;新键数较旧基线少逾 10% 须加 --force 显式确认)。
# 重生成;新键数较旧基线少逾 10% 或登记值被抬高均拒写,须加 --force 显式确认)。
game/core/action_economy.gd|quality/max-class-variables = 18
game/core/adventure_runner.gd|quality/max-branches|_apply_curio_pick = 10
game/core/adventure_runner.gd|quality/max-branches|_draw_curio_candidates = 11
game/core/adventure_runner.gd|quality/max-branches|resume_adventure = 11
game/core/adventure_runner.gd|quality/max-class-variables = 42
game/core/adventure_runner.gd|quality/max-file-length = 1051
game/core/adventure_runner.gd|quality/max-function-length|_draw_curio_candidates = 85
game/core/adventure_runner.gd|quality/max-function-length|_start_current_level = 53
game/core/adventure_runner.gd|quality/max-function-length|resume_adventure = 108
game/core/adventure_runner.gd|quality/max-local-variables|_draw_curio_candidates = 26
game/core/adventure_runner.gd|quality/max-local-variables|resume_adventure = 13
game/core/adventure_runner.gd|quality/max-public-methods = 23
game/core/adventure_runner.gd|quality/max-returns|_apply_curio_pick = 9
game/core/battle_orchestrator.gd|quality/max-branches|begin_battle = 17
@@ -141,9 +135,6 @@ game/data/schemas/reward_table.gd|quality/max-function-length|validate = 99
game/data/schemas/reward_table.gd|quality/max-local-variables|validate = 20
game/data/schemas/settlement_table.gd|quality/max-branches|validate = 16
game/data/schemas/settlement_table.gd|quality/max-class-variables = 18
game/data/schemas/shop_table.gd|quality/max-branches|validate = 22
game/data/schemas/shop_table.gd|quality/max-function-length|validate = 84
game/data/schemas/shop_table.gd|quality/max-local-variables|validate = 20
game/data/schemas/skill_table.gd|quality/max-branches|validate = 29
game/data/schemas/skill_table.gd|quality/max-function-length|validate = 80
game/data/schemas/status_effect_table.gd|quality/max-branches|validate = 12
@@ -162,8 +153,6 @@ game/presentation/hud_presenter.gd|quality/max-parameters|_refresh_out_of_battle
game/presentation/hud_presenter.gd|quality/max-parameters|refresh = 8
game/presentation/hud_presenter.gd|quality/max-parameters|tune_sim_report = 10
game/presentation/main.gd|quality/max-class-variables = 49
game/presentation/main.gd|quality/max-file-length = 1507
game/presentation/main.gd|quality/max-function-length|_build_ui = 189
game/presentation/playtest_logger.gd|quality/no-debug-print|debug 'print()' call found
game/presentation/treasure_interact_controller.gd|quality/max-local-variables|inventory_projection = 11
game/presentation/treasure_interact_controller.gd|quality/max-parameters|_init = 7
@@ -192,15 +181,10 @@ game/presentation/views/board_cell_unit.gd|quality/max-local-variables|_draw = 1
game/presentation/views/board_fx_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/board_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/board_view.gd|quality/max-branches|_paint_cell = 24
game/presentation/views/board_view.gd|quality/max-branches|_wall_tile_for = 14
game/presentation/views/board_view.gd|quality/max-class-variables = 45
game/presentation/views/board_view.gd|quality/max-file-length = 1220
game/presentation/views/board_view.gd|quality/max-function-length|_paint_cell = 139
game/presentation/views/board_view.gd|quality/max-local-variables|_digit_ink_mode = 12
game/presentation/views/board_view.gd|quality/max-local-variables|_paint_cell = 22
game/presentation/views/board_view.gd|quality/max-public-methods = 27
game/presentation/views/board_view.gd|quality/max-returns|_wall_center_tile = 7
game/presentation/views/board_view.gd|quality/max-returns|_wall_tile_for = 15
game/presentation/views/board_zone_view.gd|quality/max-function-length|_init = 59
game/presentation/views/camp_overlay_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop
game/presentation/views/camp_overlay_view.gd|quality/max-class-variables = 19
+26 -9
View File
@@ -16,19 +16,25 @@ quality/*+syntax/reserved-identifier,format/naming/order 全关)。扫描
带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键无数值。
判定:①键不在基线=新违规(NEW)红;②键在基线且双方有数值、当前数值 > 基线登记值=
恶化(WORSENED)红——独立于差集,防稳定键吞掉 104→300 行式恶化;③其余绿(改小/持平不红)。
逐条违规行带 FAIL 前缀——gate 09 的存量红指纹只取含 FAIL 的行,带前缀才能让「红项清单」
(哪几条、恶化到多少)进指纹:否则存量红期间的任何新增/进一步恶化都算同款红被放行。
基线重生成=人工专用(--refresh,附新旧对账与骤降守卫),禁在闸内自动重生成(防清基线换绿);
新键数较旧基线少逾 10% 时拒绝写入,须加 --force 显式确认(防扫面断裂静默缩基线换绿),
--force 只在 refresh 路径有效。
阈值口径=gdstyle 默认值(tools/gdstyle.toml 不设数值阈值);基线只是欠账簿,登记值只准因
还债下降,禁抬高到现状(抬高=把既有代码当尺子,默认恶化)。
基线重生成=人工专用(--refresh,附新旧对账、骤降守卫与抬升守卫),禁在闸内自动重生成
(防清基线换绿);新键数较旧基线少逾 10% 拒绝写入(防扫面断裂静默缩基线换绿);任一键登记
值被抬高同样拒绝(防以现状为尺吞掉恶化)。两条守卫都须 --force 显式确认,--force 只在
refresh 路径有效。
二进制:本机固定路径 D:/code/gdstyle/gdstyle.exe(不入库不入包),GDSTYLE_BIN 环境变量覆盖;
版本与钉定版不符即红——升级工具须重测语法兼容性并人工重生成基线。
退出码:0=无基线外新违规且无恶化;1=有新违规/有恶化/二进制缺失或版本不符/扫描面为空/
输出不可解析/refresh 骤降拒写;2=用法错。
输出不可解析/refresh 骤降或抬升拒写;2=用法错。
用法:python tools/gdstyle_gate.py # 闸判定
python tools/gdstyle_gate.py --refresh # 人工重生成基线(覆盖旧文件,附对账)
python tools/gdstyle_gate.py --refresh --force # 骤降守卫触发时显式确认写入
python tools/gdstyle_gate.py --refresh --force # 骤降/抬升守卫触发时显式确认写入
"""
import json
import os
@@ -171,6 +177,17 @@ def run_gate() -> int:
if len(current) < len(old) * SHRINK_GUARD and not force:
return fail("新键数 %d 较旧基线 %d 少逾 10%%——疑扫面断裂静默缩基线换绿,拒绝写入;"
"确认缩基线正当(如删规则/缩扫描面)须加 --force 显式确认" % (len(current), len(old)))
raised = sorted(
(k, old[k], current[k]) for k in current
if k in old and old[k] is not None and current[k] is not None and current[k] > old[k]
)
if raised and not force:
for k, was, now in raised[:MAX_REPORT]:
print(" FAIL 抬高 %s = %d(登记 %d)" % (k, now, was))
if len(raised) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(raised))
return fail("重生成会抬高 %d 条登记值——基线只准因还债下降,抬高=以现状为尺默认恶化;"
"确属阈值口径变更或经用户批准须加 --force 显式确认" % len(raised))
else:
print("[gdstyle_gate] 旧基线不存在,跳过对账(首次落位)")
write_baseline(current)
@@ -191,15 +208,15 @@ def run_gate() -> int:
red = False
if new:
for k in new[:MAX_REPORT]:
print(" NEW " + k)
print(" FAIL 新增 " + k)
if len(new) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(new))
print(" FAIL ...(截断,共 %d 条)" % len(new))
red = True
if worsened:
for k in worsened[:MAX_REPORT]:
print(" WORSENED %s = %d(基线登记 %d)" % (k, current[k], base[k]))
print(" FAIL 恶化 %s = %d(基线登记 %d)" % (k, current[k], base[k]))
if len(worsened) > MAX_REPORT:
print(" ...(截断,共 %d 条)" % len(worsened))
print(" FAIL ...(截断,共 %d 条)" % len(worsened))
red = True
if red:
return fail("基线外新违规 %d 条/恶化 %d 条——新码须修到基线口径内,恶化须还债或经用户批准随批重生成基线"
+26
View File
@@ -0,0 +1,26 @@
"""解析 Godot 的 user:// 根目录——与 game/override.cfg 的隔离口径同一份。
无 override.cfg(主工作区)→ %APPDATA%/Godot/app_userdata/<project.godot 的 config/name>;
有 override.cfg(代码面 worktree,由 tools/setup_worktree_userdir.sh 生成)→ %APPDATA%/<custom_user_dir_name>。
"""
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))
+3 -2
View File
@@ -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 = {
+8
View File
@@ -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 行被写成乱码字节(指纹漂移,
# 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。
+45
View File
@@ -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"