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()