From 0857e291d5e5a88ff90f63aa772796c70fe791c3 Mon Sep 17 00:00:00 2001 From: RCiBot Date: Thu, 12 Nov 2026 14:31:34 +0800 Subject: [PATCH] chore: add agent guidance and ignore local workspaces Document Unity and Blender MCP workflows and retain workspace initialization tooling and QA evidence. Exclude local resource copies and the credential draft from version control. --- .gitignore | 7 + AGENTS.md | 44 +++ Docs/Production/ImplementationPlan.md | 2 + Docs/Production/TreeInitializationPlan.md | 95 +++++ Docs/QA/Workspaces/Initialization.json | 87 +++++ Docs/QA/Workspaces/Initialization.md | 17 + README.md | 1 + Tools/Workspaces/InitializeTrees.py | 411 ++++++++++++++++++++++ 8 files changed, 664 insertions(+) create mode 100644 AGENTS.md create mode 100644 Docs/Production/TreeInitializationPlan.md create mode 100644 Docs/QA/Workspaces/Initialization.json create mode 100644 Docs/QA/Workspaces/Initialization.md create mode 100644 Tools/Workspaces/InitializeTrees.py diff --git a/.gitignore b/.gitignore index 835daea..716c064 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,12 @@ /[Bb]uilds/ /MemoryCaptures/ /Recordings/ +/tree1/ +/tree2/ +/tree3/ +/[Ww]ork[Tt]ree/ +/worktrees/ +/.worktrees/ .vs/ .idea/ *.csproj @@ -24,5 +30,6 @@ __pycache__/ /.env /.env.* !/.env.example +/agents.md.txt .DS_Store Thumbs.db diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bda9970 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# 项目协作约定 + +本文件适用于仓库及其子目录。默认用中文交流,按现有方案完成常规实现;涉及玩法方向、平台、存档格式等重大决策时向用户确认。用户已经授权的操作持续有效。 + +## 工程与工作入口 + +- 项目为 IRONSPORE:Windows PC 键鼠机甲搜打撤原型,含越肩和第一人称、配装、局内改装及中英双语。 +- 使用 `ProjectSettings/ProjectVersion.txt` 指定的 Unity 版本,当前为 `6000.6.8f1`。依赖版本以 `Packages/manifest.json` 和锁定文件为准。 +- 开始相关工作前按需阅读 [README](README.md)、[玩法设计](Docs/Design/GameDesign.md)、[实施计划](Docs/Production/ImplementationPlan.md) 和 [文件规范](Docs/Production/ProjectConventions.md)。沿用现有结构,避免无关重构。 + +## Unity 与 Blender + +- 引擎操作使用 **Unity MCP**。场景、组件、预制体、导入设置和构建修改前,回读目标工程的完整路径、活动场景与编辑器状态;操作后检查保存结果及 Console。 +- 多实例操作明确绑定准确的 `Name@hash`,逐次核对完整工程路径,不以项目名或路径前缀推断目标。不同目录分别维护缓存和 MCP 会话。 +- C#、USS、JSON、工具脚本和文档可以通过文件工具修改。运行时代码修改后,通过 Unity MCP 刷新并等到编译与域重载结束,再测试或构建。现有桥接入口为 `Tools/Unity/McpClient.py`。 +- 3D 建模、修整、导出使用 **Blender MCP**。先读当前文件、场景和对象;在任务自己的场景或集合中操作,保留已有未参与任务的内容。 +- Blender 源文件保存在 `SourceAssets/Blender/`,导出模型进入 `Assets/Game/Art/Models/`。尺寸以米为单位,核对朝向、碰撞和武器/驾驶舱挂点。 +- 不通过重建全部原型场景来完成局部修改;编辑器生成工具只在任务明确需要时运行。 + +## 文件、资源与数据 + +- 源码按功能放入 `Assets/Game/Scripts/`,配置放入 `Assets/Game/Data/`,场景放入 `Assets/Scenes/`,UI 和本地化沿用各自现有目录。 +- Unity 资产与对应 `.meta` 一起维护,保留 GUID。已导入资产的移动或重命名通过 Unity MCP 完成并检查引用。 +- 允许从公开网站下载免费的模型、声音、字体或代码。核实具体许可,保留原件、作者、版本/日期及 SHA-256,在 [素材台账](Docs/Production/AssetSources.md) 登记;许可原文放入 `Docs/Production/Licenses/`,发布包附必要署名。 +- 下载原件放入 `SourceAssets/ThirdParty/<发行方>/<资源包>/`,Unity 导入物放入 `Assets/ThirdParty/` 对应目录;项目适配代码和派生资源使用项目自身目录。 +- 翻译使用稳定文本键和完整句式参数,保留物品 ID、蓝图 ID、存档字段及回收事件编号。已有通知和结算也应随语言切换刷新。 +- 存档、回收去重与写入失败处理属于核心行为;修改时保留现有版本兼容,并验证状态没有重复结算。 + +## 本地资源目录与版本控制 + +- `workTree/`、`worktrees/`、`.worktrees/` 和 `tree1/`、`tree2/`、`tree3/` 是本地工作区或资源副本目录,排除在主仓库提交及云端推送之外。不要使用强制添加将其纳入 Git。 +- 已初始化的 tree 资源副本尚无独立分支或 worktree 注册。运行前核对目录内的 `WorkspaceInfo.json` 和实际路径;移动后的旧清单不能作为正确连接目标的依据。 +- 并行运行前分别隔离测试存档与 PlayerPrefs,并为各目录完成 Unity 导入和 MCP 实例绑定。资源复制通过不等于编译、运行或性能验收通过。 +- 分支、worktree 的创建和合并按用户明确要求执行。推送前检查暂存区、完整待推送路径及远程状态,使用正常推送,不擅自强制覆盖远程历史。 +- 维护忽略规则:不提交 Library、Temp、Logs、Builds、UserSettings、IDE 缓存、生成的工程文件或凭据。已整理的 Docs/QA 验收摘要、测试结果及截图可以提交。 +- 仓库地址通过 Git remote 管理。密码、令牌和带凭据的 URL 不写入本文件、源码、提交说明或可上传的日志。 + +## 验证与交付 + +- 根据实际修改选择必要检查。纯文档或 Git 配置变更检查链接、暂存范围与忽略规则;玩法、存档和本地化修改运行相关测试,修复失败后再继续。 +- 当前自动化测试在 `Assets/Tests/EditMode/`,Unity MCP 测试命令在 `Tools/Unity/Commands/Tests.json`,结果通过对应 job 查询到完成。 +- UI 变更核对实际分辨率、文本溢出、中文字符和鼠标/键盘交互。Windows 构建通过 MCP 发起并轮询到结束,启动完整产物目录验证实际行为。 +- QA 记录放入 `Docs/QA/<阶段>/`,构建产物放入 `Builds/Windows/x64/`。明确区分静态检查、编辑器测试和独立程序实测,仅记录实际完成的验证。 +- 60 FPS 与完整出击节奏为验收目标,不以未测量的推断代替结果。并行开发后的性能采样在单独运行的测试实例中进行。 diff --git a/Docs/Production/ImplementationPlan.md b/Docs/Production/ImplementationPlan.md index 0a12af8..ef296b1 100644 --- a/Docs/Production/ImplementationPlan.md +++ b/Docs/Production/ImplementationPlan.md @@ -4,6 +4,8 @@ ## 工程起点 +tree1–tree3 的普通目录与资源副本已按 [资源初始化计划](TreeInitializationPlan.md) 完成,结果见 [初始化验收](../QA/Workspaces/Initialization.md)。三个目录暂为普通资源副本,未创建分支或注册 worktree。 + 2026-11-12 增补的主页语言切换及全局双语工作按 [多语言实施计划](LocalizationPlan.md) 的 L1–L7 执行,完成后继续原有美术、实战与性能工作。 多语言增量已交付简体中文、English、主页右下角即时切换与独立偏好。验证与 Windows 产物见 [多语言验收记录](../QA/Localization/Acceptance.md)。 diff --git a/Docs/Production/TreeInitializationPlan.md b/Docs/Production/TreeInitializationPlan.md new file mode 100644 index 0000000..2034c0c --- /dev/null +++ b/Docs/Production/TreeInitializationPlan.md @@ -0,0 +1,95 @@ +# tree1–tree3 资源初始化计划 + +2026-11-12。资源初始化已按本计划执行完成:建立普通目录及资源快照,暂不创建 Git 分支、不注册 worktree,也不开始玩法优化或同时启动三个编辑器。实际结果见 [初始化验收](../QA/Workspaces/Initialization.md)。 + +## 位置与基线 + +三个目标按当前项目根目录下规划: + +| 名称 | 目标绝对路径 | 初始用途 | +| --- | --- | --- | +| tree1 | `D:/vibeCoding/unityRouge/tree1` | 完整资源基线,具体任务后续分配。 | +| tree2 | `D:/vibeCoding/unityRouge/tree2` | 完整资源基线,具体任务后续分配。 | +| tree3 | `D:/vibeCoding/unityRouge/tree3` | 完整资源基线,具体任务后续分配。 | + +执行前核查确认三个目录均不存在。参考基线为 `main` 的 `92623a1bc8678319819e3300eddf1ccff41a55e9`,Unity 版本为 `6000.6.8f1`。执行开始时冻结并记录一次基线提交,三份快照均从该提交导出,不分别读取可能变化的工作区文件。若执行时 HEAD 已更新,先将实际采用的提交写入初始化记录。 + +当前基线共 256 个已提交文件,包含源码、资源、制作源文件、工具、文档和验收证据。三份采用独立文件副本,预计合计约 126 MiB,尚不包含以后 Unity 导入生成的缓存。 + +## 每个目录的内容 + +```text +tree1/ # tree2、tree3 同样结构 + Assets/ # 场景、脚本、配置、模型、字体及全部 .meta + Packages/ # manifest 与锁定文件 + ProjectSettings/ # 当前引擎、输入、渲染和构建配置 + SourceAssets/ # Blender 源文件、字体原件与许可 + Tools/ # Unity、Blender、本地化工具 + Docs/ # 设计、规范、许可及已提交验收证据 + README.md # 基线的工程说明 + .editorconfig + .gitattributes + .gitignore + .vsconfig + WORKSPACE.md # 当前目录用途、阶段及使用边界 + WorkspaceInfo.json # tree 名称、来源提交、目标路径及状态 + InitializationManifest.json # 文件大小、SHA-256、路径适配记录 +``` + +Unity 资产及 `.meta` 原样成对复制,不重新生成 GUID。原始字体、Blender 源文件与许可完整保留,沿用 [文件规范](ProjectConventions.md) 与 [素材台账](AssetSources.md)。不使用硬链接、目录联接或符号链接共享可修改的 Assets、ProjectSettings 或 SourceAssets。 + +不复制主仓库的 `.git`、Library、Temp、Obj、Logs、UserSettings、Builds、IDE 缓存和生成的工程文件。已提交到 Docs 的验收摘要与截图属于基线文档,随快照保留。现有 Windows 成品继续位于主工程的 Builds,不为这三个资源目录重新构建。 + +在主仓库 `.gitignore` 中增加 `/tree1/`、`/tree2/`、`/tree3/`,避免后续普通提交把三份资源重复加入主仓库。三个目录此时没有独立 Git 历史;在其内部运行 Git 仍可能识别到父仓库,不能将其视为已经建立的 worktree。 + +## 初始化时处理的路径 + +已发现以下四份工具文件含主工程的绝对路径。每份副本仅将对应根路径替换为本 tree 的完整路径,记录适配前后哈希;主工程工具保持原样。 + +| 文件 | 适配内容 | +| --- | --- | +| `Tools/Unity/Commands/Build.json` | Windows 原型输出进入本 tree 的 Builds。 | +| `Tools/Unity/Commands/LocalizationBuild.json` | 多语言构建输出进入本 tree 的 Builds。 | +| `Tools/Unity/Commands/Restart.json` | 重开工程的目标指向本 tree。 | +| `Tools/Blender/CreatePrototypeMech.py` | Blender 保存和模型导出根目录指向本 tree。 | + +适配采用 JSON 解析或明确的源码常量替换,随后检查解析结果及预期差异。许可、历史验收记录和来源元数据里的原始路径属于记录内容,保持原样。初始化不执行这些工具。 + +`WorkspaceInfo.json` 记录预期 Unity 工程路径,MCP 实例 ID 保持空值,状态为 `resources-only / not-connected`。Python 服务安装与已有 Unity MCP 包版本可以复用,此阶段不复制全局 Python 环境,不修改现有 MCP 连接。 + +## 执行顺序 + +| 编号 | 工作 | 完成条件 | +| --- | --- | --- | +| T0 | 核对主仓库、基线提交、目标路径和剩余空间。 | 三个目标在预定根目录内,没有覆盖风险;记录采用的提交。 | +| T1 | 配置主仓库的三个忽略目录,建立初始化工具与临时区。 | 工具放在 `Tools/Workspaces/`,临时区放在已忽略的 `Logs/WorkspaceInitialization/`。 | +| T2 | 从冻结提交导出资源,在临时区分别生成三份独立副本。 | 已提交内容完整;没有 `.git`、Unity 缓存、构建包或共享链接。 | +| T3 | 适配四份工具路径,生成工作区说明、元数据与 SHA-256 清单。 | 每个副本的输出和重开目标均属于本 tree,原始资源内容保持。 | +| T4 | 校验文件、资产引用基础条件和目录隔离。 | 下方静态验收全部通过。 | +| T5 | 将通过校验的临时目录移到 tree1、tree2、tree3。 | 再次核对源和目标绝对路径;目标不存在才发布,不覆盖已有文件。 | +| T6 | 写入初始化验收与主仓库状态记录。 | 结果进入 `Docs/QA/Workspaces/Initialization.md`;分支列表、worktree 列表与主工程 Assets 不变。 | + +若同名目录已经存在,先检查其初始化清单。同一基线且内容完整时可以跳过;存在未知内容或差异时保留现场,不自动覆盖。重新初始化不使用删除整个工作区的方式。 + +## 静态验收 + +1. 三份目录的基线提交和复制清单一致;除说明、元数据及四份路径适配外,所有文件与源提交的 SHA-256 一致。 +2. Assets 下每个资产与目录均有对应 `.meta`,GUID 与基线一致;场景、预制体、字体配置及本地化表均存在。 +3. 包清单、锁定文件、Unity 版本、自制 Blender 源文件、原始字体和许可齐全。 +4. 四份工具的写入目标全部在自己的 tree 根目录内,不残留指向主工程的活动输出路径;历史文档中的路径单独列为来源记录。 +5. 目录之间没有共享可写链接;没有复制缓存、构建产物或 `.git`。 +6. 主仓库的 `git status` 不列出 tree 内资源;分支和 worktree 注册数量保持不变。 +7. 记录实际大小、文件数量、适配项与静态检查结果。此阶段不把文件复制成功记录为 Unity 编译、播放或构建通过。 + +## 启动并行开发前的后续工作 + +- 各 tree 首次打开 Unity 时独立解析 UPM 包、导入资源和生成缓存,再通过 Unity MCP检查场景、编译与中文字体。资源初始化不预先复制主工程 Library。 +- 每个 Unity 编辑器通过准确的 `Name@hash` 与完整工程路径绑定自己的 MCP 会话,实际发现实例后才填入连接信息。 +- 当前游戏读取 `Application.persistentDataPath`,语言偏好使用 PlayerPrefs;复制工程会保留相同的 companyName / productName。在并行运行前,通过各自 Unity MCP 配置独立的测试存档及偏好命名空间,避免互相修改主工程测试数据。 +- 需要分支时,先明确各目录的任务与修改归属,再在新的空目标目录建立真实 Git worktree。已有资源目录若产生变更,按初始化清单保存和迁移差异,经验证后再切换用途,不直接覆盖现有目录。 + +## 本次执行结果 + +T0–T6 已完成,三份均采用上述 `92623a1` 基线。每份共 259 个文件、约 41.96 MiB,其中 252 个基线文件原样保留、4 个工具文件完成路径适配,另附 3 个初始化说明与清单文件。85 个 Unity GUID 及每种语言 134 个文本键检查通过。三份总计约 125.88 MiB。 + +初始化工具为 `Tools/Workspaces/InitializeTrees.py`,可核验同基线的已有目录并跳过复制,遇到未知内容时拒绝覆盖。逐文件 SHA-256 位于各 tree 的 `InitializationManifest.json`,总结果见 [Initialization.json](../QA/Workspaces/Initialization.json)。本轮未创建分支或 worktree,未运行导入、编译、播放或构建。 diff --git a/Docs/QA/Workspaces/Initialization.json b/Docs/QA/Workspaces/Initialization.json new file mode 100644 index 0000000..afbd7bc --- /dev/null +++ b/Docs/QA/Workspaces/Initialization.json @@ -0,0 +1,87 @@ +{ + "schemaVersion": 1, + "initializedAt": "2026-11-12T13:38:14+08:00", + "runId": "20261112T133814-a6b8fb86", + "sourceRepository": "D:/vibeCoding/unityRouge", + "sourceCommit": "92623a1bc8678319819e3300eddf1ccff41a55e9", + "baselineFileCount": 256, + "baselineBytes": 43919526, + "freeDiskBytesAtStart": 236727984128, + "trees": [ + { + "name": "tree1", + "path": "D:/vibeCoding/unityRouge/tree1", + "status": "passed", + "sourceCommit": "92623a1bc8678319819e3300eddf1ccff41a55e9", + "sourceFiles": 256, + "totalFiles": 259, + "bytes": 43998875, + "adaptedFiles": 4, + "unchangedFiles": 252, + "unityMetaFiles": 85, + "uniqueUnityGuids": 85, + "localizationKeysPerLocale": 134, + "independentOrdinaryFiles": true, + "cacheAndGitExcluded": true, + "hashValidation": "passed", + "manifestSha256": "d96f3b6a02432e63a81378482130e99f9cfced89f6eaff6350bf9ef4df465416", + "operation": "created" + }, + { + "name": "tree2", + "path": "D:/vibeCoding/unityRouge/tree2", + "status": "passed", + "sourceCommit": "92623a1bc8678319819e3300eddf1ccff41a55e9", + "sourceFiles": 256, + "totalFiles": 259, + "bytes": 43998875, + "adaptedFiles": 4, + "unchangedFiles": 252, + "unityMetaFiles": 85, + "uniqueUnityGuids": 85, + "localizationKeysPerLocale": 134, + "independentOrdinaryFiles": true, + "cacheAndGitExcluded": true, + "hashValidation": "passed", + "manifestSha256": "516af0ab018df468b7492b9dedc8e8c50c8c148fd451db31e2720312658e91bc", + "operation": "created" + }, + { + "name": "tree3", + "path": "D:/vibeCoding/unityRouge/tree3", + "status": "passed", + "sourceCommit": "92623a1bc8678319819e3300eddf1ccff41a55e9", + "sourceFiles": 256, + "totalFiles": 259, + "bytes": 43998875, + "adaptedFiles": 4, + "unchangedFiles": 252, + "unityMetaFiles": 85, + "uniqueUnityGuids": 85, + "localizationKeysPerLocale": 134, + "independentOrdinaryFiles": true, + "cacheAndGitExcluded": true, + "hashValidation": "passed", + "manifestSha256": "2c2551dcfca3a25a545f1157880ee1a85a621a6b605c3a189554e76a241d4465", + "operation": "created" + } + ], + "primaryAssetsUnchanged": true, + "gitHeadUnchanged": true, + "gitBranchesUnchanged": true, + "gitWorktreeRegistrationsUnchanged": true, + "gitIndexUnchanged": true, + "primaryGitState": { + "head": "92623a1bc8678319819e3300eddf1ccff41a55e9", + "branches": "refs/heads/main 92623a1bc8678319819e3300eddf1ccff41a55e9", + "worktrees": "worktree D:/vibeCoding/unityRouge\nHEAD 92623a1bc8678319819e3300eddf1ccff41a55e9\nbranch refs/heads/main", + "index": "100644 ce0f2fe3a3224b6a9da8a44211fe7281eb88b6ca 0\t.editorconfig\n100644 5a32c35596df9c7bb2b12e65cba7518bcdd131fa 0\t.gitattributes\n100644 835daea1efd93d0cf2c36aec61bf80ee83bfe5c9 0\t.gitignore\n100644 f019fd0ad121bf5f9c0eb35e29cd09d56ce712b3 0\t.vsconfig\n100644 4d9a6d13d7b512ed4cc6b212b1a6796ee5006617 0\tAssets/Editor.meta\n100644 5a7a30458278a009d81445df95570eadb0a3d5de 0\tAssets/Editor/Ironspore.Editor.asmdef\n100644 b483b6649b2c0be153cdf49055801920856d8fd0 0\tAssets/Editor/Ironspore.Editor.asmdef.meta\n100644 b2fac9d94fba43e0a5558d0b20af89f5d3ccb942 0\tAssets/Editor/LocalizationBuilder.cs\n100644 3ca38addbc5dd8469bcb8451e764110baac0eb9e 0\tAssets/Editor/LocalizationBuilder.cs.meta\n100644 6f03e47e293e8b7d4a7525f7d6841aaa5d746161 0\tAssets/Editor/LocalizationSmoke.cs\n100644 f6e8187f89a55f91b4b22350744e80418becfde3 0\tAssets/Editor/LocalizationSmoke.cs.meta\n100644 1ac8cff8290d79f4c0f1e08841a978b8f5145fa2 0\tAssets/Editor/PrototypeBuilder.cs\n100644 702d005ff3654c839359661a7cdda04d69cf55df 0\tAssets/Editor/PrototypeBuilder.cs.meta\n100644 b651bb2858916f4182d5a5ac2df20e52c525a2a0 0\tAssets/Editor/PrototypeSmoke.cs\n100644 3cb72e4690fc66f72e1d37808c1f6ce6e951948f 0\tAssets/Editor/PrototypeSmoke.cs.meta\n100644 71d7f648c7a5d23e76913ef458aa63fa0e0417f5 0\tAssets/Editor/RecoverySmoke.cs\n100644 f4c16a2d453456b1f63347be0130381f168d21b4 0\tAssets/Editor/RecoverySmoke.cs.meta\n100644 1c096b367d8e7b93f73e9e0b8d71360b709e55e4 0\tAssets/Editor/RouteSmoke.cs\n100644 b55319c6cf98f8b37c8ac1adfc9def3282173080 0\tAssets/Editor/RouteSmoke.cs.meta\n100644 9cd84dda4b091719f875b6eea30eec844806e6e3 0\tAssets/Game.meta\n100644 5b80df187ac20cbc74f09fa03824b826776278d2 0\tAssets/Game/Art.meta\n100644 992591785d1dbb3c179cd771d94f8a8e51f4dc83 0\tAssets/Game/Art/Models.meta\n100644 3d9316906c72ba367724b822ad5e672c07aa6b71 0\tAssets/Game/Art/Models/Mechs.meta\n100644 b4e75f9ac1962656c7556e6d8fd48081771c22a5 0\tAssets/Game/Art/Models/Mechs/Mech_Scout_A.fbx\n100644 6f59764360a61b799081342ab0a5a5409af9f108 0\tAssets/Game/Art/Models/Mechs/Mech_Scout_A.fbx.meta\n100644 b4feb519e0adaf3b4d0a190ae32c0471f108fc16 0\tAssets/Game/Data.meta\n100644 9c851273a93d42c6477f29c5d34c01826e183f88 0\tAssets/Game/Data/Equipment.meta\n100644 6f6f0df5141777dda2660a3c7d0af77d087f5d9c 0\tAssets/Game/Data/Equipment/coil.asset\n100644 242766bab38d4e1c313cd28fb372805ab8f1db59 0\tAssets/Game/Data/Equipment/coil.asset.meta\n100644 992fe1a3da79de7b756a65561bcc8a1e3034fe72 0\tAssets/Game/Data/Equipment/composite_guard.asset\n100644 965eceb3012d3341b387657108560222305531f6 0\tAssets/Game/Data/Equipment/composite_guard.asset.meta\n100644 31341cdfc72829dd056a4c61adf34c5fd00130bc 0\tAssets/Game/Data/Equipment/efficient.asset\n100644 69b3f115b33eed29ec4a4f9b44cdf6b7899bde01 0\tAssets/Game/Data/Equipment/efficient.asset.meta\n100644 fae239c28c5b6f77e0b5948d29e5f2d281c0d539 0\tAssets/Game/Data/Equipment/guided.asset\n100644 ba78a29f7739f181a2a382bec292cfb87a05f8da 0\tAssets/Game/Data/Equipment/guided.asset.meta\n100644 b1d7377b92bf8398c665ade8b374a0c9110445fb 0\tAssets/Game/Data/Equipment/pulse.asset\n100644 f150b533e16ccf8b3be4fbf4b5f543278ddafe43 0\tAssets/Game/Data/Equipment/pulse.asset.meta\n100644 9404f80a1e752c7877093f2ec6f286e5f238c641 0\tAssets/Game/Data/Equipment/rapid.asset\n100644 151c8a8c59a71d788c43e4bde8439a57dc5737ae 0\tAssets/Game/Data/Equipment/rapid.asset.meta\n100644 78bd195b5c4e0594f8341b6934d20ab06e7e7df5 0\tAssets/Game/Data/Equipment/reactive_guard.asset\n100644 bfdaf61b06f085d4f06bf41551c2be7482eaacc0 0\tAssets/Game/Data/Equipment/reactive_guard.asset.meta\n100644 5cdf7dc8e5747dbc7feefbabb89f508b300f387f 0\tAssets/Game/Data/Equipment/siege.asset\n100644 147a9a816787b00eac7f891f8eb92adf99f9cbc2 0\tAssets/Game/Data/Equipment/siege.asset.meta\n100644 2ef2a4b3fda1b900153998c290d5e8467f8839f6 0\tAssets/Game/Data/Equipment/vector.asset\n100644 88b2a33b76c83cad9f82c898cefbabc5e6466bab 0\tAssets/Game/Data/Equipment/vector.asset.meta\n100644 2a79100ea7ae63b0e957060f2d2347fc74625097 0\tAssets/Game/Data/Mechs.meta\n100644 193d321509cdfc17477d493b9de26791783367fb 0\tAssets/Game/Data/Mechs/Armor.mat\n100644 7c8c4756d49fb49ea244c8fb753faa35f2400d41 0\tAssets/Game/Data/Mechs/Armor.mat.meta\n100644 adf18943d326bd2d7753d14fe6200b395b149809 0\tAssets/Game/Data/Mechs/Chassis_0.asset\n100644 43d1a0df21648e8d835c49bb19862f5977e30367 0\tAssets/Game/Data/Mechs/Chassis_0.asset.meta\n100644 d70ed697c1a4f302d742d885fecb5bd46b386926 0\tAssets/Game/Data/Mechs/Chassis_1.asset\n100644 0b9c2a654c08bd72f9a3fd7d6a5c612d45779077 0\tAssets/Game/Data/Mechs/Chassis_1.asset.meta\n100644 f5b0d049a431289ab1cdd166ba682cfdfc3ef850 0\tAssets/Game/Data/Mechs/Chassis_2.asset\n100644 ac166fd4b323d1326d01f50fe51fa2d6af9df881 0\tAssets/Game/Data/Mechs/Chassis_2.asset.meta\n100644 973fe30a687d9bb9f04c1cd2f7d43172f137dca4 0\tAssets/Game/Data/Mechs/Mechanism.mat\n100644 296267db6ad6f1cf93051f3e76b48390aff16ad5 0\tAssets/Game/Data/Mechs/Mechanism.mat.meta\n100644 fa3d517281a450cfe86bbf3c7eca25f5fd0bfe44 0\tAssets/Game/Data/Mechs/Reactor.mat\n100644 b651104df9ac119ae8a29c8611718df2169da347 0\tAssets/Game/Data/Mechs/Reactor.mat.meta\n100644 20c5dbdde8e42be9a02e8ec0d9197b751320dca3 0\tAssets/Game/Data/Mechs/Trim.mat\n100644 1e1eecbf347659db1bb5feb61d6850d376fa336a 0\tAssets/Game/Data/Mechs/Trim.mat.meta\n100644 031c19ea85d7607be53414eb021dd076bce736da 0\tAssets/Game/Input.meta\n100644 398eaecbcce6faa2803257289d9001884eea0bd3 0\tAssets/Game/Input/GameInput.inputactions\n100644 864d0a397ac8026030205565ef7a0262f222e433 0\tAssets/Game/Input/GameInput.inputactions.meta\n100644 98624b0942b9301f9ee8a2bd97bea66ef90b853d 0\tAssets/Game/Localization.meta\n100644 ed0e6ebb93c69bf61a8a9f3523640d68e3067296 0\tAssets/Game/Localization/LocalizationCatalog.asset\n100644 2d7da7bde88c5e4a4169bf56f94aa8de720cdf47 0\tAssets/Game/Localization/LocalizationCatalog.asset.meta\n100644 e1f4cfdc21c8d528070b471cef733719fe4b0c0f 0\tAssets/Game/Localization/Tables.meta\n100644 b60691fc47036fe297c46abf822378b0a63ad9cd 0\tAssets/Game/Localization/Tables/en.json\n100644 4eb59f34deb0a843d5a98905bef8435e9b0d7ef0 0\tAssets/Game/Localization/Tables/en.json.meta\n100644 861fd8b9ab4ad4e4961cffc2f5c832fce36af33a 0\tAssets/Game/Localization/Tables/zh-Hans.json\n100644 3ee87eb6a54c3d2e5720702c5a6ff8d73e7e6981 0\tAssets/Game/Localization/Tables/zh-Hans.json.meta\n100644 fe12a89d0d009d83d64fef130d7712fe54afe479 0\tAssets/Game/Prefabs.meta\n100644 605f6e095eb1d56af8851f465d73c3648a44ae8f 0\tAssets/Game/Prefabs/Mechs.meta\n100644 139938b31b890c7c31ec875423f9cc6f2bf4bcaa 0\tAssets/Game/Prefabs/Mechs/Mech_Scout_A.prefab\n100644 d9c541c31bc194bf5c876e3ff7aeefc63282946c 0\tAssets/Game/Prefabs/Mechs/Mech_Scout_A.prefab.meta\n100644 985618cef5231dcdbb8ff9a9fa2928fdaf21dbdb 0\tAssets/Game/Scripts.meta\n100644 d8f066534ddb1ec5904323ddb4a9f7b041ae3718 0\tAssets/Game/Scripts/Combat.meta\n100644 2ca705facab0fff03f42357666663e75655f8bb8 0\tAssets/Game/Scripts/Combat/EnemyUnit.cs\n100644 c0cbb796500d0f67de211c0624d09de8b00613a3 0\tAssets/Game/Scripts/Combat/EnemyUnit.cs.meta\n100644 2257efc765d4ecaf86de76bb09feae8b8174c3be 0\tAssets/Game/Scripts/Combat/WorldFactory.cs\n100644 8e68d34ad3ba7e855a74b399ddaedc1a2b458aec 0\tAssets/Game/Scripts/Combat/WorldFactory.cs.meta\n100644 9c46cc440b3a279b4fa502e512bb5f53fdf85568 0\tAssets/Game/Scripts/Equipment.meta\n100644 040b9198735cbf023486af2b5beb31012ee4f841 0\tAssets/Game/Scripts/Equipment/EquipmentDefinition.cs\n100644 87ee7da12f61c2b84cc810397dd6dd462976256e 0\tAssets/Game/Scripts/Equipment/EquipmentDefinition.cs.meta\n100644 172e0436812a1f726ffb92b3e5ab261597edfd0e 0\tAssets/Game/Scripts/Ironspore.Runtime.asmdef\n100644 9c4e0962c464d00e01c3f9c27701ba28219b7786 0\tAssets/Game/Scripts/Ironspore.Runtime.asmdef.meta\n100644 a1c69ae828f30ee2a7b97cfd282d017e687ef8b6 0\tAssets/Game/Scripts/Localization.meta\n100644 1c5605c8b9d42232cd9a2a0693978af5004f4c55 0\tAssets/Game/Scripts/Localization/GameLocalization.cs\n100644 9ab0e4eae8442b60313fc0f5d2b6156fd3842269 0\tAssets/Game/Scripts/Localization/GameLocalization.cs.meta\n100644 98d7fbb992db31ec78b04321d1d58e6d85147b6d 0\tAssets/Game/Scripts/Localization/LocalizationCatalog.cs\n100644 bb247ae79b8977c89d58322b928cd46aec10e30b 0\tAssets/Game/Scripts/Localization/LocalizationCatalog.cs.meta\n100644 c519183b9f201da33182ade1f9caba05681bc686 0\tAssets/Game/Scripts/Localization/LocalizedWorldLabel.cs\n100644 39db6771b4cda7ecc1aded3117a7195daddf2032 0\tAssets/Game/Scripts/Localization/LocalizedWorldLabel.cs.meta\n100644 6f63e9b3116535ab4af1fb6d05c9013b38855d7f 0\tAssets/Game/Scripts/Mechs.meta\n100644 64ff51a1254a13a43b698d9768af5f7175e31383 0\tAssets/Game/Scripts/Mechs/ChassisDefinition.cs\n100644 4f03e4e83705fff0d22fd6d45514cc62f6b2fc02 0\tAssets/Game/Scripts/Mechs/ChassisDefinition.cs.meta\n100644 2b49b8f468e1572e6412d2d65e2ceccc845b4885 0\tAssets/Game/Scripts/Mechs/MechPilot.cs\n100644 9a2dfe082265976b5faaa9d711eb4db867ea92da 0\tAssets/Game/Scripts/Mechs/MechPilot.cs.meta\n100644 3eedc6bcc70d866ed9609f81428b1c3db86498c3 0\tAssets/Game/Scripts/Runs.meta\n100644 b9a639481a777228172b7048933e78c14e065bae 0\tAssets/Game/Scripts/Runs/GameSession.cs\n100644 f0bde2d689ac869e57baa667a46b8b5832883e9e 0\tAssets/Game/Scripts/Runs/GameSession.cs.meta\n100644 045866a507b41a605d9a61f8197fa45e8ccd5ccc 0\tAssets/Game/Scripts/Runs/WarzoneDirector.cs\n100644 39e977bcc9cf64f258bca1c36b4d07d8ee9d598d 0\tAssets/Game/Scripts/Runs/WarzoneDirector.cs.meta\n100644 1f5381dc63361dc096d78a44b98151587d1ddd19 0\tAssets/Game/Scripts/Save.meta\n100644 9ec2cd897d9852e26550ef5c3d3e8016e3364b75 0\tAssets/Game/Scripts/Save/ProfileStore.cs\n100644 71a07b482dccc8f70719d697d6f61d035c8d124a 0\tAssets/Game/Scripts/Save/ProfileStore.cs.meta\n100644 7a367a5e1c0d1f57d0a5b7e31bcf7281165238dc 0\tAssets/Game/Scripts/UI.meta\n100644 7bfe1adf561d3b798cc529437527cb3192be7044 0\tAssets/Game/Scripts/UI/GameHud.cs\n100644 b8e57b53f6ec976f85fee83dc2988f36d6240deb 0\tAssets/Game/Scripts/UI/GameHud.cs.meta\n100644 cc12deefb89e85954decaea5555cbffeb608ccd6 0\tAssets/Game/UI.meta\n100644 5defef740f2ba118bcba968dc3095b930d70491b 0\tAssets/Game/UI/Fonts.meta\n100644 b8b87b19631098e90d47f24dc706e4ac2fdad823 0\tAssets/Game/UI/Fonts/GameFont.asset\n100644 24ea729248058ce979d826d72e4152635f8b65db 0\tAssets/Game/UI/Fonts/GameFont.asset.meta\n100644 9db508a8080d9014f2535989abd0601f89a3fe4f 0\tAssets/Game/UI/Fonts/GameTextSettings.asset\n100644 dc3a374a6f044ef2fe343dcbdbcfa8a2315f319d 0\tAssets/Game/UI/Fonts/GameTextSettings.asset.meta\n100644 b66329fd9db218fcb50758195fef4cac7103342d 0\tAssets/Game/UI/GameHud.uss\n100644 7eb2acd476f6c98a96665313953a7e28c47440eb 0\tAssets/Game/UI/GameHud.uss.meta\n100644 3a70b09ed54265af418f4424d779fa37b4bf41a8 0\tAssets/Game/UI/GamePanelSettings.asset\n100644 4f3c7a7a983eaecee908b81e231a392f98ee0824 0\tAssets/Game/UI/GamePanelSettings.asset.meta\n100644 51924cb33eebde3c11c50c714826553d60ecef70 0\tAssets/Game/UI/GameTheme.tss\n100644 da9735bf6fb96996a0ac77f5cd63b481ca775e6b 0\tAssets/Game/UI/GameTheme.tss.meta\n100644 44600eefc994d94f03e5f53e1ad9b7652d65d1b6 0\tAssets/Scenes.meta\n100644 927640a86cd416830c7640615102cd8981ad6d02 0\tAssets/Scenes/Bootstrap.unity\n100644 68a4a27c2a5d8e8501f057bc4d5a396fbf6eeb1d 0\tAssets/Scenes/Bootstrap.unity.meta\n100644 18f71532d80dc9e3545aeaa98b89504bac4d93fd 0\tAssets/Scenes/MechTestRange.unity\n100644 56a652cb73cbb4599c76d500579e531d77e12ae5 0\tAssets/Scenes/MechTestRange.unity.meta\n100644 e71fa5a7c2a88a2c97691992d332a858b2931237 0\tAssets/Scenes/SampleScene.unity\n100644 cb1905d5f4a49d9b643f43b3ad5279b18d53e2db 0\tAssets/Scenes/SampleScene.unity.meta\n100644 927640a86cd416830c7640615102cd8981ad6d02 0\tAssets/Scenes/WarzonePrototype.unity\n100644 aa1f194282c96e497b82722eacd0560a569260c0 0\tAssets/Scenes/WarzonePrototype.unity.meta\n100644 e2e05d9cddff8c81e73ad59b5236d336c1804fca 0\tAssets/Tests.meta\n100644 6e7445259f6c3a3587c391fb0cfdfc40869779e5 0\tAssets/Tests/EditMode.meta\n100644 098ae5eb54b01b8044da3f05b9dbddd798593e1e 0\tAssets/Tests/EditMode/Ironspore.Tests.asmdef\n100644 146d8c74a9e78630fa3a73dba81875d20774127c 0\tAssets/Tests/EditMode/Ironspore.Tests.asmdef.meta\n100644 3745fe3c4122499e5b80df1f4478e4ed0976443c 0\tAssets/Tests/EditMode/LocalizationTests.cs\n100644 652166a3c87bde4cf2b856293ff26a50fff1e707 0\tAssets/Tests/EditMode/LocalizationTests.cs.meta\n100644 8e9fc588d341f062297b56eac2e510058fa1b846 0\tAssets/Tests/EditMode/ProfileStoreTests.cs\n100644 3cba0a193576b4ed1bdd2379c8c4e5a3d0091f32 0\tAssets/Tests/EditMode/ProfileStoreTests.cs.meta\n100644 ef7cc7b463f3236fff19782fe821a95556c11d89 0\tAssets/ThirdParty.meta\n100644 38dc44f6b60f8ff9e333a1539bcc8759c81bee00 0\tAssets/ThirdParty/Adobe.meta\n100644 f281941b06eff7be9a2256932723e89110464c87 0\tAssets/ThirdParty/Adobe/NotoSansSC.meta\n100644 cc79aef4ca2dc72d3880c59909d1259596c24eb5 0\tAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf\n100644 4fec01ad4d1bbb3a5abbb70b766d43678c778c85 0\tAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf.meta\n100644 cea9f3aa0ee4a5d17f5237d30a36213e284988cf 0\tAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt\n100644 8f9baca13671e5caaaa6c001268fe72ebdfe14a9 0\tAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt.meta\n100644 3a0be7aa11862f54953a9e6140107bc6e1d10176 0\tDocs/Design/GameDesign.md\n100644 5893a633d2535ea6569e45206e11e9acd957e882 0\tDocs/Production/AssetSources.md\n100644 0a12af891a1d9c2121f9a76876e999ea365097a2 0\tDocs/Production/ImplementationPlan.md\n100644 cea9f3aa0ee4a5d17f5237d30a36213e284988cf 0\tDocs/Production/Licenses/Adobe/NotoSansSC/OFL.txt\n100644 6d8bd8505b0f981dcc2f06558b34bf51c50667e3 0\tDocs/Production/Licenses/Adobe/NotoSansSC/Provenance.json\n100644 468c75ef98346b86d6519dc1cd976ae643558b7e 0\tDocs/Production/LocalizationPlan.md\n100644 2ff2d20f044ca4f79b511ff607201f91f095d57e 0\tDocs/Production/ProjectConventions.md\n100644 da0b914776428bfb789d17e8bce49266577b57ce 0\tDocs/QA/Environment/Connections.md\n100644 b11f5ecaeba20b9c49bc23d030011b632171403b 0\tDocs/QA/Localization/Acceptance.md\n100644 d20e30d94e96975bd1e91ebf9554f23a9cc92232 0\tDocs/QA/Localization/Logs/Artifacts.json\n100644 4323281c427ba68d432fcfde1ddc47364c836c13 0\tDocs/QA/Localization/Logs/Build.json\n100644 3b55d60687f240d0ef186212255361639daae0a9 0\tDocs/QA/Localization/Logs/EditorConsole.json\n100644 63f1da459999b7f9bd27f8b2a995db649404d0bf 0\tDocs/QA/Localization/Screenshots/cargo-en.png\n100644 c90abe0fd0e510862640247ebdf42705a4f8d47a 0\tDocs/QA/Localization/Screenshots/cargo-zh.png\n100644 0d06a08a7cdd42a12102eb86d17e1f6ab9da2afe 0\tDocs/QA/Localization/Screenshots/hangar-en-1080.png\n100644 95e1126f21137a401f514ba67ee61728910768f1 0\tDocs/QA/Localization/Screenshots/hangar-zh-1080.png\n100644 e244195c15c304f2cf4e418da5aef99bf5fe20cb 0\tDocs/QA/Localization/Screenshots/hangar-zh-1280x720.png\n100644 76e3eb7b0536f91fbbb97cda9e402c77d8df785d 0\tDocs/QA/Localization/Screenshots/hangar-zh-2560x1080.png\n100644 a05dcf90c38f2f33bc0a4a5729a060117e5f8179 0\tDocs/QA/Localization/Screenshots/hangar-zh-2560x1440.png\n100644 edd6c379a33913bd20dbe9d64d9d31efd2373b19 0\tDocs/QA/Localization/Screenshots/hud-en.png\n100644 c8b8ba29ae6babc8f90a2c034654fe5ad353242f 0\tDocs/QA/Localization/Screenshots/hud-zh.png\n100644 9cdfe4cca0145cbb645c6ec1b4cce7aa5dbd8367 0\tDocs/QA/Localization/Screenshots/language-menu-en.png\n100644 739df3cf6893a3c3da1eef300b2bc5c761f8ec6d 0\tDocs/QA/Localization/Screenshots/pause-en.png\n100644 09b6f7a5bfb8a9bcdc7226836d903b52edf198a1 0\tDocs/QA/Localization/Screenshots/pause-zh.png\n100644 8d6bec02ca9e3da4c36f0226fa7a167df02c7179 0\tDocs/QA/Localization/Screenshots/refit-en.png\n100644 c65a5588f440f2dd38b17e6a3008008f250ecbd8 0\tDocs/QA/Localization/Screenshots/refit-zh.png\n100644 a302d838d0d603b62fd8d54824cf5abc37db9df3 0\tDocs/QA/Localization/Screenshots/report-en.png\n100644 ee400d8845393c5c9a938425010a2416d3e1acc5 0\tDocs/QA/Localization/Screenshots/report-zh.png\n100644 29020dbc0596277941e052dfce3b157ffad07bab 0\tDocs/QA/Localization/Screenshots/routes-en.png\n100644 e8073c35654db83b084b217e13cf2c357da2e50e 0\tDocs/QA/Localization/Screenshots/routes-zh.png\n100644 493b0723d5c5c37b44cf8b7e7203857fa874d67d 0\tDocs/QA/Localization/TestResults/EditMode.json\n100644 2f5bb9a42d32ae9bffbc2d3bce9e185e4e38199b 0\tDocs/QA/Localization/TestResults/Integration.json\n100644 0f0d06ec580a189e841a2a9efef20d98f26ddf25 0\tDocs/QA/Localization/TestResults/WindowsInteraction.json\n100644 0ed22c1d7543bce8e53532465138f28665e24d59 0\tDocs/QA/Prototype/Acceptance.md\n100644 4461ae5ec331b0a139efe50eaad24f23695e7f03 0\tDocs/QA/Prototype/ArtifactManifest.json\n100644 50bc8344a5aa339d42fa80f3b7198c04960541bf 0\tDocs/QA/Prototype/Logs/WindowsBuild.json\n100644 ae7fb7a8c84768d3195748e512fb186fac81396d 0\tDocs/QA/Prototype/Screenshots/cockpit.png\n100644 60f1a8b4d4283befe8065acdd1ce61169f4028d5 0\tDocs/QA/Prototype/Screenshots/hangar-1.png\n100644 62d94b1a2d74188cef46584e52be9819bdad3db6 0\tDocs/QA/Prototype/Screenshots/hangar.png\n100644 8969eab00ca14f33f08dfbd4b740f36423d569c5 0\tDocs/QA/Prototype/Screenshots/shoulder.png\n100644 0180efb723ba482f8ce002541c404c3e8e7501ce 0\tDocs/QA/Prototype/TestResults/KeyboardPlayback.json\n100644 6135cbe54dfbfbad8b420f9485c7c9f72f8e6fa7 0\tDocs/QA/Prototype/TestResults/RecoveryLedger.json\n100644 bbce0dbd896d8d9bad90deff5c78542ab3c42b76 0\tDocs/QA/Prototype/TestResults/RecoveryPlayback.json\n100644 24c7d4f7c084e186fd70243de0c1e92b9b1c1aa5 0\tDocs/QA/Prototype/TestResults/RouteStructure.json\n100644 b815989f7e9505ef42520fd8615c29e44c45af1e 0\tPackages/manifest.json\n100644 1384dc395944fac08d343cda28d5ba26bfe6c029 0\tPackages/packages-lock.json\n100644 50b4625b0374d5aa81daeb6320a5f4443d5efc9a 0\tProjectSettings/AudioManager.asset\n100644 e7886b266a005f4d9d80f2fef8d1649dcfd3ed2b 0\tProjectSettings/ClusterInputManager.asset\n100644 39290c5b20fc95f8256ae6d2d380a488d10fe12a 0\tProjectSettings/DynamicsManager.asset\n100644 84cfa086d4559645ada7efe4bd9df7b30d07c587 0\tProjectSettings/EditorBuildSettings.asset\n100644 23224e35dd65652acc31f11662ea9987431e1d7f 0\tProjectSettings/EditorSettings.asset\n100644 61c884d674a385e2c32ec55157d06f9c8bbc71b6 0\tProjectSettings/GraphicsSettings.asset\n100644 8068b2058b089f9973b15f83648e58cd238688f0 0\tProjectSettings/InputManager.asset\n100644 e1bbfb1a41958f69fbd9c39fb50c7986791b33c2 0\tProjectSettings/MemorySettings.asset\n100644 2a936644e0d87339f17484efc93de6f0de5774b1 0\tProjectSettings/MultiplayerManager.asset\n100644 2e2e36968e36401ed2c4cf0f4d545e3d38ad25e9 0\tProjectSettings/NavMeshAreas.asset\n100644 8f3f280ea5765efd314bf176106f402c4e11293a 0\tProjectSettings/PackageManagerSettings.asset\n100644 67aaf1c16ec59c136026ac01bb1d0d52a369f24b 0\tProjectSettings/Physics2DSettings.asset\n100644 049c7454b5cb1220c8440514c5628548672a661e 0\tProjectSettings/PhysicsCoreProjectSettings2D.asset\n100644 67a94daefe2e6bce3ec73546ad7cda94f702ad22 0\tProjectSettings/PresetManager.asset\n100644 e0e893172db8e8c8483732be73d518b3ac39d337 0\tProjectSettings/ProjectAuditorSettings.asset\n100644 3515e0b2b26e62bca09bd11c438dbfbbb402bb75 0\tProjectSettings/ProjectSettings.asset\n100644 eafb27581e0f3705e243ebe0bb3860cbb177c80f 0\tProjectSettings/ProjectVersion.txt\n100644 64f8abaf15b65739baa144aa04ab955160ea4ad3 0\tProjectSettings/QualitySettings.asset\n100644 ede5887b3a3e5e7ce4e90f0d893a1e4cdb2c7451 0\tProjectSettings/SceneTemplateSettings.json\n100644 6abf33d85028220e7832b659c2085a7b49dc21a2 0\tProjectSettings/TagManager.asset\n100644 2e23a1f42187cc2b6b25e899a7caaede6e8a0bd1 0\tProjectSettings/TimeManager.asset\n100644 c00917065e26cbcfc0a7eff8822baf3d6b65db9f 0\tProjectSettings/UnityConnectSettings.asset\n100644 d51c4d354f41f9742b852e7e99de8b3becb84cdb 0\tProjectSettings/VFXManager.asset\n100644 979fd8eca84fae8dac89c1021ef6049be95ee5f6 0\tProjectSettings/VersionControlSettings.asset\n100644 00e0b5f0b07c8ec40ca48f94fab76a38ff2b53d0 0\tREADME.md\n100644 069723bac2917447e4ebf68c99b98e0399b645b4 0\tSourceAssets/Blender/Mechs/Mech_Scout_A.blend\n100644 cc79aef4ca2dc72d3880c59909d1259596c24eb5 0\tSourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf\n100644 cea9f3aa0ee4a5d17f5237d30a36213e284988cf 0\tSourceAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt\n100644 cfb401d12c8dac2055438e43b15ff67185db4de1 0\tTools/Blender/CreatePrototypeMech.py\n100644 f2f975a778843a76cea39cf6bde81be499297373 0\tTools/Localization/ArchiveFont.py\n100644 30e56bd9e93733830077dfdf732416043887315e 0\tTools/Localization/BuildTables.py\n100644 dcfa7b5b90e84b7c331180b20e3f418cf1040662 0\tTools/Unity/Commands/Build.json\n100644 8c4be91839a41dadaa02c3cc5a1e032dac1380fe 0\tTools/Unity/Commands/BuildStatus.json\n100644 8b1f643eb63bbd73f09f37a6db6ecf19af75bac8 0\tTools/Unity/Commands/Capture.json\n100644 aa50abf29f234c3fe846e2bf7d9514bc170c89fb 0\tTools/Unity/Commands/Compile.json\n100644 1f5c29d19f7310f4f610fce153d41be7aac481a5 0\tTools/Unity/Commands/Console.json\n100644 368b9e5ee3a25660fdb1f116d0d74e7504d33e72 0\tTools/Unity/Commands/Environment.json\n100644 2c3c629cf62918c1a91cf8b6dadf7194275214d5 0\tTools/Unity/Commands/FixImports.json\n100644 d908df6db915eb0d40f9f19bc1954d8dc47fc69c 0\tTools/Unity/Commands/Generate.json\n100644 7808367ca49ec0098a73ec9e699fd6e3262b7355 0\tTools/Unity/Commands/InspectModel.json\n100644 89de384cf90b57fd5be35e0471de9740262a7b06 0\tTools/Unity/Commands/InspectReticle.json\n100644 d7719641c4efe48d3c58788e4f10c7ef6513b215 0\tTools/Unity/Commands/LocalizationBuild.json\n100644 2511b30ac559234a3d48b048713565334f6e8412 0\tTools/Unity/Commands/LocalizationCapture.json\n100644 77173d4ed762b943452bdc4b9e254ba080bc38b0 0\tTools/Unity/Commands/LocalizationConfigure.json\n100644 0a420382adf3986dfe69972330098e5fde4e3398 0\tTools/Unity/Commands/LocalizationInspectFonts.json\n100644 340b3680a1a19e98da22c222b19e561e500347e6 0\tTools/Unity/Commands/LocalizationInspectResolution.json\n100644 70296979d0c81b119bd54efb83adc7f3422a111b 0\tTools/Unity/Commands/LocalizationInspectView.json\n100644 2e15cb5af2fd538cd44146b4f3fb6972980d1f8f 0\tTools/Unity/Commands/LocalizationInstall.json\n100644 228152dfa15f98ccf97ba8f8ba02bfcfbc977a49 0\tTools/Unity/Commands/LocalizationPackageStatus.json\n100644 5fe5165e4c0eab74e63295f1b51455f8410177c4 0\tTools/Unity/Commands/LocalizationPackages.json\n100644 61ef71aadeceef406225a4471406d331c5d7ab8b 0\tTools/Unity/Commands/LocalizationRefresh.json\n100644 f115ed60cea256879462b98c4836be5abf3bbde8 0\tTools/Unity/Commands/LocalizationSmoke.json\n100644 c1eeaef2d3741c7f043afd0a52021221b67609ed 0\tTools/Unity/Commands/Play.json\n100644 b8e78219b6ef6afca0b1bd882526f4f8e49f2ff5 0\tTools/Unity/Commands/RecoverySmoke.json\n100644 d1b3e544123620ddffe6954952c062de58a7b892 0\tTools/Unity/Commands/Refresh.json\n100644 49875133f697d710ad374e68fa271aafc00b143a 0\tTools/Unity/Commands/Restart.json\n100644 8d5ac788f7cffaee0afe93966d14bdc71bdb24c6 0\tTools/Unity/Commands/Routes.json\n100644 387cc444115b2980229bb3bd248a5d1a3dd4e80d 0\tTools/Unity/Commands/Smoke.json\n100644 0bdaf1c336afff7c595c1b68610a3cd185a59752 0\tTools/Unity/Commands/StartRange.json\n100644 af12cc9da11907fc0b3d94cd1cc06e7fe50ee42c 0\tTools/Unity/Commands/Stop.json\n100644 e6f9c407d804bd36d5a37d03f2e146292d0138cb 0\tTools/Unity/Commands/TestStatus.json\n100644 fa4eaba7ba39a2b28ddb32ea7d16fc460ebdeef4 0\tTools/Unity/Commands/Tests.json\n100644 044888c7cf6bffaa3ec5bd58eea5c939a037a1c1 0\tTools/Unity/Commands/View.json\n100644 0a82de8d16213554895b3e34e9bf734fe261d7f1 0\tTools/Unity/McpClient.py\n100644 2bbddfa518e0e17f9042c98c91213e8a7853023d 0\tTools/Unity/WindowsReadme.txt" + }, + "scope": "Static resource initialization only; Unity/Blender/MCP were not started.", + "pending": [ + "Unity package resolution/import/compilation", + "MCP explicit instance binding", + "Separate test saves and PlayerPrefs before parallel play", + "Branches/worktrees only when requested" + ] +} diff --git a/Docs/QA/Workspaces/Initialization.md b/Docs/QA/Workspaces/Initialization.md new file mode 100644 index 0000000..71e3fe5 --- /dev/null +++ b/Docs/QA/Workspaces/Initialization.md @@ -0,0 +1,17 @@ +# tree1–tree3 资源初始化验收 + +时间:2026-11-12T13:38:14+08:00(Asia/Hong_Kong)。基线:`92623a1bc8678319819e3300eddf1ccff41a55e9`。 + +| 目录 | 文件数 | 大小 | Unity .meta | 结果 | +| --- | --- | --- | --- | --- | +| tree1 | 259 | 41.96 MiB | 85 | created / passed | +| tree2 | 259 | 41.96 MiB | 85 | created / passed | +| tree3 | 259 | 41.96 MiB | 85 | created / passed | + +每份包含 256 个基线文件、4 份工具路径适配及 3 份工作区说明/清单文件。其中 252 个基线文件逐字节保留;适配文件与源、目标路径均按清单验证。 + +检查通过:逐文件 SHA-256、资产与 .meta 配对、GUID 唯一且与基线一致、中英文表键一致、源模型/字体/许可/包锁完整、无共享可写链接、无缓存或构建包、父仓库忽略规则生效。主工程 Assets、HEAD、分支、worktree 注册及 Git 暂存区均保持原状。 + +三个目录均为普通资源副本,没有独立 Git 历史。MCP 实例未连接;Unity 导入、编译、播放和构建未执行;并行运行前仍需隔离存档及 PlayerPrefs。 + +完整记录见 [Initialization.json](Initialization.json),目录内的 InitializationManifest.json 提供每个基线文件与适配项的哈希。执行计划见 [TreeInitializationPlan.md](../../Production/TreeInitializationPlan.md)。 diff --git a/README.md b/README.md index 00e0b5f..9911801 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Windows PC 键鼠原型,工程版本为 Unity 6000.6.8f1。机体支持越肩 ## 工程与验证 +- [AGENTS.md](AGENTS.md) 保存 Unity MCP、Blender MCP、资产存放、验证和 Git 提交的协作约定。 - [玩法设计](Docs/Design/GameDesign.md) 与 [开发任务](Docs/Production/ImplementationPlan.md) 保存约定和阶段验收。 - [文件规范](Docs/Production/ProjectConventions.md) 与 [素材台账](Docs/Production/AssetSources.md) 记录模型、代码和声音的来源与存放路径。 - Tools/Unity/McpClient.py 使用已安装的 Unity MCP 服务。命令文件位于 Tools/Unity/Commands,Unity 对象与场景由 MCP 操作。 diff --git a/Tools/Workspaces/InitializeTrees.py b/Tools/Workspaces/InitializeTrees.py new file mode 100644 index 0000000..58121b5 --- /dev/null +++ b/Tools/Workspaces/InitializeTrees.py @@ -0,0 +1,411 @@ +"""Initialize three ordinary resource copies from a frozen Git commit. + +Run from the primary repository. This script creates no branches or worktrees, +does not launch Unity/Blender, and never overwrites an existing resource copy. +""" +import argparse +import ast +from datetime import datetime, timedelta, timezone +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +import shutil +import stat +import subprocess +import uuid +import zipfile + + +ROOT = Path(__file__).resolve().parents[2] +NAMES = ('tree1', 'tree2', 'tree3') +ADAPTED = ( + 'Tools/Unity/Commands/Build.json', + 'Tools/Unity/Commands/LocalizationBuild.json', + 'Tools/Unity/Commands/Restart.json', + 'Tools/Blender/CreatePrototypeMech.py', +) +GENERATED = ('WORKSPACE.md', 'WorkspaceInfo.json', 'InitializationManifest.json') +FORBIDDEN = {'.git', 'library', 'temp', 'obj', 'logs', 'usersettings', 'builds', '.vs', '.idea', 'memorycaptures', 'recordings'} +HK = timezone(timedelta(hours=8), 'Asia/Hong_Kong') + + +def git(*args, check=True): + return subprocess.run(['git', '-C', str(ROOT), *args], capture_output=True, check=check) + + +def git_text(*args): + return git(*args).stdout.decode('utf-8').strip() + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def file_sha(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def safe_child(base, path): + """Resolve paths before any directory publication/move or file creation.""" + base = base.resolve() + resolved = path.resolve() + if resolved == base or not resolved.is_relative_to(base): + raise RuntimeError(f'Path leaves the allowed directory: {path}') + return resolved + + +def is_link(path): + attributes = getattr(path.lstat(), 'st_file_attributes', 0) + return path.is_symlink() or bool(attributes & getattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT', 0x400)) + + +def write_json(path, data): + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') + + +def main_assets_state(): + return {p.relative_to(ROOT).as_posix(): file_sha(p) for p in sorted((ROOT / 'Assets').rglob('*')) if p.is_file()} + + +def git_state(): + return { + 'head': git_text('rev-parse', 'HEAD'), + 'branches': git_text('for-each-ref', '--format=%(refname) %(objectname)', 'refs/heads'), + 'worktrees': git_text('worktree', 'list', '--porcelain'), + 'index': git_text('ls-files', '--stage'), + } + + +def baseline_files(commit, run): + modes = {} + for record in git('ls-tree', '-r', '-z', commit).stdout.split(b'\0'): + if not record: + continue + meta, raw_path = record.split(b'\t', 1) + mode, kind, _ = meta.decode('ascii').split() + name = raw_path.decode('utf-8') + path = PurePosixPath(name) + if path.is_absolute() or '..' in path.parts or '\\' in name or ':' in name: + raise RuntimeError(f'Invalid archive path: {name}') + if path.parts[0].lower() in FORBIDDEN or path.parts[0] in NAMES: + raise RuntimeError(f'Generated directory is tracked in baseline: {name}') + if mode not in ('100644', '100755') or kind != 'blob': + raise RuntimeError(f'Only ordinary files are supported: {mode} {name}') + modes[name] = mode + archive_path = run / 'baseline.zip' + with archive_path.open('xb') as stream: + subprocess.run(['git', '-C', str(ROOT), 'archive', '--format=zip', commit], stdout=stream, check=True) + with zipfile.ZipFile(archive_path) as archive: + files = {i.filename: archive.read(i) for i in archive.infolist() if not i.is_dir()} + if set(files) != set(modes): + raise RuntimeError('Archive does not match the frozen Git tree.') + if set(GENERATED) & set(files): + raise RuntimeError('Workspace metadata would overwrite baseline files.') + for name in ADAPTED: + if name not in files: + raise RuntimeError(f'Expected tool is missing: {name}') + return files + + +def source_root(files): + text = files[ADAPTED[3]].decode('utf-8') + match = re.search(r"^root_path = Path\('([^']+)'\)$", text, re.MULTILINE) + if not match: + raise RuntimeError('The Blender root constant is not recognized.') + return match.group(1) + + +def adapted_files(files, destination): + before_root = source_root(files) + after_root = destination.as_posix() + output = dict(files) + records = [] + for name in ADAPTED: + if name.endswith('.json'): + commands = json.loads(files[name].decode('utf-8')) + if len(commands) != 1: + raise RuntimeError(f'Expected one command in {name}') + arguments = commands[0]['arguments'] + if name.endswith('/Restart.json'): + previous = arguments['code'] + expected = f'UnityEditor.EditorApplication.OpenProject("{before_root}")' + replacement = f'UnityEditor.EditorApplication.OpenProject("{after_root}")' + if previous.count(expected) != 1: + raise RuntimeError(f'Unexpected restart target in {name}') + arguments['code'] = previous.replace(expected, replacement) + clone = json.loads(json.dumps(commands)) + clone[0]['arguments']['code'] = clone[0]['arguments']['code'].replace(replacement, expected) + else: + previous = arguments['output_path'] + relative = PurePosixPath(previous).relative_to(PurePosixPath(before_root)) + if relative.parts[0] != 'Builds' or '..' in relative.parts: + raise RuntimeError(f'Unexpected build output in {name}') + arguments['output_path'] = destination.joinpath(*relative.parts).as_posix() + safe_child(destination, Path(arguments['output_path'])) + clone = json.loads(json.dumps(commands)) + clone[0]['arguments']['output_path'] = previous + if clone != json.loads(files[name].decode('utf-8')): + raise RuntimeError(f'Unexpected semantic change to {name}') + output[name] = (json.dumps(commands, ensure_ascii=False, indent=2) + '\n').encode('utf-8') + else: + previous = files[name].decode('utf-8') + expected = f"root_path = Path('{before_root}')" + replacement = f"root_path = Path('{after_root}')" + if previous.count(expected) != 1: + raise RuntimeError('Unexpected Blender root constant.') + result = previous.replace(expected, replacement) + ast.parse(result, filename=name) + if result.replace(replacement, expected) != previous: + raise RuntimeError('Unexpected Blender source edit.') + output[name] = result.encode('utf-8') + if output[name] == files[name]: + raise RuntimeError(f'Expected a path adaptation: {name}') + records.append({'path': name, 'fromRoot': before_root, 'toRoot': after_root, + 'sourceSha256': sha(files[name]), 'sha256': sha(output[name])}) + return output, records + + +def workspace_info(name, destination, commit, initialized): + return { + 'schemaVersion': 1, 'name': name, 'stage': 'resources-only', 'initializedAt': initialized, + 'sourceRepository': ROOT.as_posix(), 'sourceCommit': commit, + 'projectRoot': destination.as_posix(), 'unityVersion': '6000.6.8f1', + 'git': {'kind': 'ordinary-directory', 'branchCreated': False, 'worktreeRegistered': False}, + 'mcp': {'expectedProjectRoot': destination.as_posix(), 'instanceId': None, 'connected': False}, + 'runtimeDataIsolation': {'status': 'pending-before-play', + 'reason': 'The copied companyName/productName and PlayerPrefs namespace still match the baseline.'}, + 'unityImport': 'not-run', 'unityCompilation': 'not-run', 'unityBuild': 'not-run', + } + + +def create_copy(staging, destination, name, files, commit, initialized): + staging.mkdir() + output, adaptations = adapted_files(files, destination) + for relative, data in output.items(): + target = safe_child(staging, staging / relative) + target.parent.mkdir(parents=True, exist_ok=True) + with target.open('xb') as stream: + stream.write(data) + # Git cannot store empty folders; Unity folder metadata can describe one. + for relative, data in files.items(): + if relative.startswith('Assets/') and relative.endswith('.meta') and re.search(rb'^folderAsset:\s*yes\s*$', data, re.MULTILINE): + safe_child(staging, staging / relative[:-5]).mkdir(parents=True, exist_ok=True) + write_json(staging / 'WorkspaceInfo.json', workspace_info(name, destination, commit, initialized)) + (staging / 'WORKSPACE.md').write_text( + f'# {name} 资源目录\n\n' + f'基线:`{commit}`。Unity:`6000.6.8f1`。目标:`{destination.as_posix()}`。\n\n' + '本目录是普通资源副本,未建立独立 Git 仓库、分支或 worktree。' + '目录内部的 Git 命令可能识别到父仓库,不应作为独立版本记录使用。\n\n' + '资源、GUID、制作源文件与许可已复制;四份工具的输出或重开路径已指向本目录。' + '路径适配及逐文件校验见 InitializationManifest.json。历史文档中的路径仍记录原来源。\n\n' + 'MCP 尚未连接,Unity 尚未导入、编译或构建。首次使用需要为本目录选择明确的 Unity 实例与完整路径。' + '默认存档和 PlayerPrefs 命名空间尚未隔离,完成隔离前不要并行运行游戏。\n\n' + '后续按主工程 Docs/Production/TreeInitializationPlan.md 迁移至正式开发工作区。\n', + encoding='utf-8') + generated = [{'path': relative, 'bytes': (staging / relative).stat().st_size, + 'sha256': file_sha(staging / relative)} for relative in GENERATED[:2]] + manifest = { + 'schemaVersion': 1, 'sourceCommit': commit, 'projectRoot': destination.as_posix(), + 'sourceFileCount': len(files), 'sourceBytes': sum(map(len, files.values())), + 'adaptations': adaptations, + 'files': [{'path': path, 'bytes': len(data), 'sourceSha256': sha(files[path]), + 'sha256': sha(data), 'adapted': path in ADAPTED} for path, data in sorted(output.items())], + 'generatedFiles': generated, + 'selfHash': 'Recorded in the primary repository initialization report, not inside this manifest.', + } + write_json(staging / 'InitializationManifest.json', manifest) + + +def validate_copy(directory, destination, name, files, commit): + output, adaptations = adapted_files(files, destination) + metadata = json.loads((directory / 'WorkspaceInfo.json').read_text(encoding='utf-8')) + if metadata['name'] != name or metadata['sourceCommit'] != commit or metadata['projectRoot'] != destination.as_posix(): + raise RuntimeError(f'Existing workspace has a different identity: {directory}') + if metadata['stage'] != 'resources-only' or metadata['git'] != {'kind': 'ordinary-directory', 'branchCreated': False, 'worktreeRegistered': False}: + raise RuntimeError(f'Workspace has advanced beyond resource initialization: {directory}') + if metadata['mcp'] != {'expectedProjectRoot': destination.as_posix(), 'instanceId': None, 'connected': False}: + raise RuntimeError(f'Workspace connection state changed: {directory}') + manifest = json.loads((directory / 'InitializationManifest.json').read_text(encoding='utf-8')) + if manifest['sourceCommit'] != commit or manifest['projectRoot'] != destination.as_posix() or manifest['adaptations'] != adaptations: + raise RuntimeError(f'Manifest baseline or adaptations differ: {directory}') + if manifest['sourceFileCount'] != len(files) or manifest['sourceBytes'] != sum(map(len, files.values())): + raise RuntimeError(f'Manifest baseline count or size differs: {directory}') + recorded = {entry['path']: entry for entry in manifest['files']} + if len(recorded) != len(files) or set(recorded) != set(output): + raise RuntimeError(f'Manifest file set differs: {directory}') + found = [] + for path in directory.rglob('*'): + safe_child(directory, path) + if is_link(path): + raise RuntimeError(f'Linked path is not allowed: {path}') + if path.is_file(): + if path.stat().st_nlink != 1: + raise RuntimeError(f'Hard-linked file is not allowed: {path}') + found.append(path.relative_to(directory).as_posix()) + if set(found) != set(files) | set(GENERATED): + raise RuntimeError(f'Unexpected or missing files in {directory}') + if any((directory / item).exists() for item in ('.git', 'Library', 'Temp', 'Obj', 'Logs', 'UserSettings', 'Builds', '.vs', '.idea')): + raise RuntimeError(f'Generated directory was copied: {directory}') + for relative, expected in output.items(): + path = directory / relative + entry = recorded[relative] + if entry != {'path': relative, 'bytes': len(expected), 'sourceSha256': sha(files[relative]), + 'sha256': sha(expected), 'adapted': relative in ADAPTED}: + raise RuntimeError(f'Manifest hash is not derived from the baseline: {relative}') + if path.stat().st_size != len(expected) or file_sha(path) != sha(expected): + raise RuntimeError(f'File does not match baseline/adaptation: {path}') + if relative.endswith('.json'): + json.loads(path.read_text(encoding='utf-8-sig')) + if relative.startswith('Tools/') and relative.endswith('.py'): + ast.parse(path.read_text(encoding='utf-8-sig'), filename=relative) + if {entry['path'] for entry in manifest['generatedFiles']} != set(GENERATED[:2]) or len(manifest['generatedFiles']) != 2: + raise RuntimeError('Generated metadata records differ.') + for entry in manifest['generatedFiles']: + path = directory / entry['path'] + if path.stat().st_size != entry['bytes'] or file_sha(path) != entry['sha256']: + raise RuntimeError(f'Generated metadata was changed: {path}') + assets = directory / 'Assets' + meta_count = 0 + guids = set() + for path in assets.rglob('*'): + if path.name.endswith('.meta'): + meta_count += 1 + if not path.with_suffix('').exists(): + raise RuntimeError(f'Orphan Unity metadata: {path}') + match = re.search(r'^guid: ([a-f0-9]{32})$', path.read_text(encoding='utf-8'), re.MULTILINE) + if not match or match.group(1) in guids: + raise RuntimeError(f'Missing or duplicate Unity GUID: {path}') + guids.add(match.group(1)) + elif not Path(str(path) + '.meta').is_file(): + raise RuntimeError(f'Unity asset has no metadata: {path}') + required = ( + 'Assets/Scenes/Bootstrap.unity', 'Assets/Scenes/MechTestRange.unity', 'Assets/Scenes/WarzonePrototype.unity', + 'Assets/Game/UI/Fonts/GameFont.asset', 'Assets/Game/UI/Fonts/GameTextSettings.asset', + 'Assets/Game/UI/GamePanelSettings.asset', 'Assets/Game/Localization/LocalizationCatalog.asset', + 'Assets/Game/Localization/Tables/en.json', 'Assets/Game/Localization/Tables/zh-Hans.json', + 'Assets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf', 'Assets/ThirdParty/Adobe/NotoSansSC/OFL.txt', + 'SourceAssets/Blender/Mechs/Mech_Scout_A.blend', 'SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf', + 'Docs/Production/Licenses/Adobe/NotoSansSC/OFL.txt', 'Packages/manifest.json', 'Packages/packages-lock.json', + ) + if any(not (directory / path).is_file() for path in required): + raise RuntimeError('Required engine resource, source or license is missing.') + if 'm_EditorVersion: 6000.6.8f1' not in (directory / 'ProjectSettings/ProjectVersion.txt').read_text(): + raise RuntimeError('Unexpected Unity version.') + tables = [json.loads((directory / f'Assets/Game/Localization/Tables/{code}.json').read_text(encoding='utf-8'))['entries'] for code in ('en', 'zh-Hans')] + keys = [{entry['key'] for entry in entries} for entries in tables] + if keys[0] != keys[1] or any(len(entries) != len(key_set) for entries, key_set in zip(tables, keys)): + raise RuntimeError('Localization table keys do not match.') + return {'name': name, 'path': destination.as_posix(), 'status': 'passed', 'sourceCommit': commit, + 'sourceFiles': len(files), 'totalFiles': len(found), 'bytes': sum((directory / item).stat().st_size for item in found), + 'adaptedFiles': len(ADAPTED), 'unchangedFiles': len(files) - len(ADAPTED), + 'unityMetaFiles': meta_count, 'uniqueUnityGuids': len(guids), 'localizationKeysPerLocale': len(keys[0]), + 'independentOrdinaryFiles': True, 'cacheAndGitExcluded': True, 'hashValidation': 'passed', + 'manifestSha256': file_sha(directory / 'InitializationManifest.json')} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--base', default='HEAD', help='HEAD or a hexadecimal Git commit ID.') + args = parser.parse_args() + if not re.fullmatch(r'HEAD|[a-fA-F0-9]{7,40}', args.base): + raise RuntimeError('Use HEAD or a hexadecimal commit ID for --base.') + if Path(git_text('rev-parse', '--show-toplevel')).resolve() != ROOT: + raise RuntimeError('Run this tool from the primary repository, not a resource copy.') + commit = git_text('rev-parse', '--verify', args.base + '^{commit}') + state_before = git_state() + assets_before = main_assets_state() + targets = {} + for name in NAMES: + raw = ROOT / name + if raw.exists() or raw.is_symlink(): + if is_link(raw) or not raw.is_dir(): + raise RuntimeError(f'Target is not an ordinary directory: {raw}') + target = safe_child(ROOT, raw) + if target.parent != ROOT or target.name != name: + raise RuntimeError(f'Target must be a direct child of the primary repository: {target}') + if git('check-ignore', '-q', '--', name + '/Assets').returncode != 0: + raise RuntimeError(f'Add /{name}/ to the primary .gitignore before initialization.') + if git_text('ls-files', '--', name): + raise RuntimeError(f'Target already contains tracked files: {name}') + targets[name] = target + initialized = datetime.now(HK).isoformat(timespec='seconds') + run_id = datetime.now(HK).strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8] + logs = safe_child(ROOT, ROOT / 'Logs/WorkspaceInitialization') + run = safe_child(logs, logs / run_id) + run.mkdir(parents=True) + files = baseline_files(commit, run) + required_bytes = sum(map(len, files.values())) * (sum(not p.exists() for p in targets.values()) + 1) + 64 * 1024 * 1024 + free_bytes = shutil.disk_usage(ROOT).free + if free_bytes < required_bytes: + raise RuntimeError(f'Not enough free disk space: {free_bytes} < {required_bytes}') + prepared = {} + outcomes = {} + # All three copies are validated before publishing any new directory. + for name, destination in targets.items(): + if destination.exists(): + outcomes[name] = 'verified-existing' + validate_copy(destination, destination, name, files, commit) + prepared[name] = destination + else: + staging = safe_child(run, run / name) + create_copy(staging, destination, name, files, commit, initialized) + validate_copy(staging, destination, name, files, commit) + prepared[name] = staging + outcomes[name] = 'created' + results = [] + for name, destination in targets.items(): + source = prepared[name] + if source != destination: + # Recheck resolved absolute source and destination immediately before moving. + if safe_child(run, source) != source or safe_child(ROOT, destination) != destination: + raise RuntimeError('Publication paths changed.') + if destination.exists() or destination.is_symlink(): + raise RuntimeError(f'Target appeared during preparation; refusing to overwrite: {destination}') + source.rename(destination) + result = validate_copy(destination, destination, name, files, commit) + result['operation'] = outcomes[name] + if git('check-ignore', '-q', '--', name + '/WorkspaceInfo.json').returncode != 0: + raise RuntimeError('Published copy is not ignored by the primary repository.') + results.append(result) + if git_state() != state_before: + raise RuntimeError('Primary HEAD, branches, worktrees or index changed during initialization.') + if main_assets_state() != assets_before: + raise RuntimeError('Primary Assets changed during initialization.') + report = { + 'schemaVersion': 1, 'initializedAt': initialized, 'runId': run_id, + 'sourceRepository': ROOT.as_posix(), 'sourceCommit': commit, + 'baselineFileCount': len(files), 'baselineBytes': sum(map(len, files.values())), + 'freeDiskBytesAtStart': free_bytes, 'trees': results, + 'primaryAssetsUnchanged': True, 'gitHeadUnchanged': True, 'gitBranchesUnchanged': True, + 'gitWorktreeRegistrationsUnchanged': True, 'gitIndexUnchanged': True, + 'primaryGitState': state_before, + 'scope': 'Static resource initialization only; Unity/Blender/MCP were not started.', + 'pending': ['Unity package resolution/import/compilation', 'MCP explicit instance binding', + 'Separate test saves and PlayerPrefs before parallel play', 'Branches/worktrees only when requested'], + } + write_json(run / 'Result.json', report) + qa = safe_child(ROOT, ROOT / 'Docs/QA/Workspaces') + qa.mkdir(parents=True, exist_ok=True) + write_json(qa / 'Initialization.json', report) + rows = '\n'.join(f"| {r['name']} | {r['totalFiles']} | {r['bytes'] / 1048576:.2f} MiB | {r['unityMetaFiles']} | {r['operation']} / passed |" for r in results) + (qa / 'Initialization.md').write_text( + '# tree1–tree3 资源初始化验收\n\n' + f'时间:{initialized}(Asia/Hong_Kong)。基线:`{commit}`。\n\n' + '| 目录 | 文件数 | 大小 | Unity .meta | 结果 |\n| --- | --- | --- | --- | --- |\n' + rows + '\n\n' + f'每份包含 {len(files)} 个基线文件、4 份工具路径适配及 3 份工作区说明/清单文件。' + f'其中 {len(files) - 4} 个基线文件逐字节保留;适配文件与源、目标路径均按清单验证。\n\n' + '检查通过:逐文件 SHA-256、资产与 .meta 配对、GUID 唯一且与基线一致、' + '中英文表键一致、源模型/字体/许可/包锁完整、无共享可写链接、无缓存或构建包、' + '父仓库忽略规则生效。主工程 Assets、HEAD、分支、worktree 注册及 Git 暂存区均保持原状。\n\n' + '三个目录均为普通资源副本,没有独立 Git 历史。MCP 实例未连接;' + 'Unity 导入、编译、播放和构建未执行;并行运行前仍需隔离存档及 PlayerPrefs。\n\n' + '完整记录见 [Initialization.json](Initialization.json),目录内的 InitializationManifest.json ' + '提供每个基线文件与适配项的哈希。执行计划见 [TreeInitializationPlan.md](../../Production/TreeInitializationPlan.md)。\n', + encoding='utf-8') + print(json.dumps({key: report[key] for key in ('runId', 'sourceCommit', 'baselineFileCount', 'trees', 'primaryAssetsUnchanged', 'gitBranchesUnchanged', 'gitWorktreeRegistrationsUnchanged')}, ensure_ascii=False, indent=2)) + + +if __name__ == '__main__': + main()