# RA3 Apt CLI — 工作计划 > 将 ra3aptxmleditor 的 .apt ↔ 人类可读 XML 双向转换流程 CLI 化。 > 基于 `ra3apt-cli/` 目录下的 Node.js 项目。 --- ## 0. 项目渊源 本 CLI 工具基于两个上游项目: - **jonwil/cc3tools** — 提供 `apt2xml.exe`(.apt → raw XML)和 `xml2apt.exe`(raw XML → .apt),是本工具的二进制转换引擎。 - **utunnels/ra3aptxmleditor** — 基于 nw.js 的 RA3 .apt 图形化编辑器,其 `preprocess.html` 中的 `xmladdhint()`(raw XML → 人类可读 XML)和 `xmlcleanhint()`(人类可读 XML → raw XML)是本 CLI 核心逻辑的直接来源。 本 CLI 将 ra3aptxmleditor 的编辑流程从 nw.js 桌面 GUI 迁移到命令行环境,使用 cheerio 替代 jQuery,保持与原始工具的行为一致性。 --- ## 1. 总体决策记录 ### 1.1 项目定位 - **CLI 工具行为与现有 nw.js GUI 完全一致**,包括读写 manifest XML、.dat、.ru 文件 - **两阶段转换模式**:`ra3apt-cli expand` (apt → 人类可读 XML) 和 `ra3apt-cli build` (人类可读 XML → apt) - **仅 Windows**(cc3tools 提供的是 Windows .exe) ### 1.2 输出文件命名 | 操作 | 默认输出 | 说明 | |------|---------|------| | `expand` | `_edit.xml` | 人类可读 XML | | `build` | `.apt` + `.const` | 二进制 .apt + 常量表 | 用户可自由指定 `-o` 覆盖默认名。 ### 1.3 文件冲突策略 **人类可读 XML 与 manifest XML 不会冲突**,因为: - manifest XML 是源目录中的 `[name].xml`(AssetDeclaration 格式,由 RA3 Mod SDK 管理) - apt raw XML 是 `%TEMP%/[name].xml`(临时文件,用户不可见) - 人类可读 XML 默认是 `[name]_edit.xml`(不同文件名) - CLI 写回 manifest XML 的行为与现有 GUI 完全一致(第 3089 行) - `.ru` 几何数据内嵌在人类可读 XML 的 `` 元素中,build 时不再依赖源目录中的 `.ru` 文件 ### 1.4 依赖库 | 库 | 用途 | 选择原因 | |----|------|---------| | `cheerio@1.0.0-rc.12` | DOM 操作(替代 jQuery) | 99% API 兼容,已验证全部 15 类用法 | | `commander` | CLI 框架 | 最流行的 Node.js CLI 框架 | ### 1.5 CDATA 方案(修订) 原始代码用 `CDATASection` 包裹 action 汇编代码。CLI 改用**先 text node + 后处理 CDATA 包装**的方式。 | 方向 | 原始 | CLI | |------|------|-----| | expand (写入) | `xmldoc.createCDATASection(text)` | `v.empty().text(astr)` cheerio 序列化后,用正则将 action/initaction/clipaction 的内容替换为 CDATA | | build (回读) | `$(el).text()` 自动解码 CDATA | cheerio 在 xmlMode 下自动将 CDATA 视为 text node,`.text()` 正常提取 | **关键细节**: - cheerio 不直接支持创建 CDATA 节点,所以用正则后处理实现 - 正则必须匹配带属性的标签(如 ``) - `clipactions`(复数)不会被误匹配,因为正则要求标签名完整后跟空格或 `>` - `&` 双编码问题通过 `decodeAttrEntities()` 在 `actiontostring` 中解决 ### 1.6 错误处理 - 原始代码中的 `alert()` 全部改为 **`throw new Error(msg)`** - CLI 入口层 `catch` 后打印到 **stderr** 并以 **exit code 1** 退出 - 覆盖全部 15 处 alert(重复 ID、跳转目标未找到、语法错误、超出 255 等) - 仅 `console.log`/`console.warn` 的诊断信息保留(非致命,不影响流程) ### 1.7 浏览器特有 API 等价替换表 | 浏览器/nw.js API | CLI 替换 | 出现次数 | |-----------------|---------|:-------:| | `$.parseXML(xmlStr)` | `cheerio.load(xmlStr, { xmlMode: true })` | 4 | | `new XMLSerializer().serializeToString(doc)` | `$.xml()` | 2 | | `xmldoc.createCDATASection(text)` | 正则后处理替换:`/([\s\S]*?)<\/tag>/` → `` | 1 | | `xmldoc.createElement(tag)` | `$('<' + tag + '>', $)` | 3 | | `$(el).attr(name)` / `.attr(name, val)` | 不变 | ~60 | | `alert(msg)` | `throw new Error(msg)` | ~15 | | `escape(t)` | `encodeURIComponent(t)` | 1 | | `el.outerHTML` | `$.html(el)` | 3 | | `el.previousSibling` | `el.prev`(cheerio 节点) | 2 | | `el.textContent` | `el.data`(文本节点) | 1 | | `el.cloneNode()` | `$.clone(el)[0]` 或 `cheerio` 原生 clone | 1 | | `editor.getValue()` / `editor.setValue()` | `fs.readFileSync()` / `fs.writeFileSync()` | GUI 专属 | | nw.js / CodeMirror / Canvas / jQuery UI | **全部省略** | GUI 专属 | --- ## 2. 关键代码定位(preprocess.html) | 模块 | 行号 | 说明 | |------|:----:|------| | 常量表 — `ActionNames` | 1484-1619 | opcode 名称表 | | 常量表 — `ActionAligns` | 1627-1647 | 哪些 action 需要对齐 | | 常量表 — `ActionSizes` | 1648-1835 | 每个 action 的固定长度 | | 常量表 — flags 枚举 | 250-354 | PlaceObjectFlags / ClipEventFlags / ButtonRecordFlags / ButtonActionFlags / FunctionPreloadFlags / ButtonInput | | 辅助函数 — `el()` / `bad()` | 2283-2288 | 元素创建 | | 辅助函数 — `endian()` / `valuetoflags()` / `flagstovalue()` | 356-396 | 字节序/flag 转换 | | 辅助函数 — `encodeXML()` / `mkstr()` | 1856-1866 | 字符串编码 | | 辅助函数 — 对齐计算 | 1968-2001 | `alnsize()` / `calcaln()` / `thisaln()` / `nextaln()` / `copyindent()` / `indentstr()` | | 核心 — `xmladdhint()` (expand) | 2023-2280 | raw XML → 人类可读 XML | | 核心 — `actiontostring()` | 1885-1967 | action 节点 → 文本汇编语句 | | 核心 — `xmlcleanhint()` (build) | 2597-3095 | 人类可读 XML → raw XML | | 核心 — `stringtoaction()` | 2290-2582 | 文本汇编语句 → action 节点 | | 核心 — .const 二进制写入 | 2741-2766 | 常量表二进制序列化 | | 核心 — manifest / .dat 生成 | 2964-3092 | 写回 manifest XML、.dat、.ru | | 核心 — `loadapt()` (打开流程) | 488-508 | apt → raw XML 的编排 | --- ## 3. 进度追踪 > 当前状态:✅ **全部完成** ### Phase 1 — 项目脚手架与常量提取 ✅ - [x] 创建 `ra3apt-cli/` 项目结构(已完成 `package.json` 初始化 + `cheerio` 安装) - [x] 安装 `commander` 依赖 - [x] 提取常量表到 `lib/constants.js`: - `ActionNames` / `ActionNames2`(新旧两套 opcode 名称) - `ActionSizes` / `ActionAligns` - `PlaceObjectFlags` / `ClipEventFlags` / `ButtonRecordFlags` / `ButtonActionFlags` / `FunctionPreloadFlags` / `ButtonInput` - [x] 创建 CLI 入口文件 `bin/ra3apt.js`,挂载 `expand` 和 `build` 子命令 ### Phase 2 — 工具函数层 ✅ - [x] 提取通用工具函数到 `lib/utils.js`: - `endian()` / `valuetoflags()` / `flagstovalue()` - `encodeXML()` / `mkstr()` / `el()` / `bad()` / `makelabel()` - `alnsize()` / `calcaln()` / `thisaln()` / `nextaln()` / `copyindent()` / `indentstr()` - `rgba()` / `getrgba()` / `cleanrgba()` / `getmatrix()` / `cleanmatrix()` - [x] 适配浏览器 DOM 原生属性访问 → cheerio 等价写法 - [x] 编写 `expand/builder` 两流程共享的临时目录管理、exe 调用、文件复制逻辑 ### Phase 3 — 实现 `ra3apt-cli expand` ✅ - [x] 实现 `expand()` 流程:复制 .apt + .const → 运行 `apt2xml.exe` → 读取 manifest XML + .dat - [x] 移植 `xmladdhint()` 全部逻辑: - 常量展开 - 跳转 label 解析 - action 汇编化 + text node 写入 - flags 符号化 - image / shape 纹理解析 - 编辑者友好清理 - [x] 移植 `actiontostring()` 全部九种 action 分支 - [x] 序列化输出人类可读 XML ### Phase 4 — 实现 `ra3apt-cli build` ✅ - [x] 移植 `xmlcleanhint()` 全部逻辑: - 文本汇编解析 + `stringtoaction()` - 常量池重构建 + .const 二进制写入(`Buffer` API,Node.js 原生) - character ID 重编号 - 跳转偏移量解析 - function size 计算 - frame 编号 - flags 数值化 - placeobject flags 构建 - shape/ru/image 处理 + manifest / .dat / .ru 生成 - 矩阵/颜色属性拆解和清理 - [x] 运行 `xml2apt.exe` 生成 .apt + .const - [x] 写回 manifest XML、.dat、.ru 文件 ### Phase 5 — 端到端测试与文档 ✅ - [x] 从 `assets/` 获取 3 组测试数据(ig_HUD、fem_m_gameSetup、main_mouse) - [x] 验证完整回路:`.apt` → expand → `_edit.xml` → build → `.apt` → apt2xml → raw XML 对比 - [x] **全部 3 组测试通过,raw XML 完全一致** - [x] 清理测试临时文件 - [x] 编写 README 与 CLI 帮助文本 ### Phase 6 — Bug 修复与完善 ✅ - [x] **Phase 6.1 — `parseArgs` 引号保留**:`parseArgs()` 返回的参数字符串两端保留引号,导致 `str` 属性带多余引号 - [x] **Phase 6.2 — 标签名不匹配**:build 产生 `data`/`noarg` 标签但 cc3tools 需要 `byte`/`short`/`float`/`long` - [x] **Phase 6.3 — placeobject flag 计算缺失 rotm00→matrix 转换**:人类可读 XML 使用 `rotm00` 格式,但 flag 计算需 `matrix` 格式 - [x] **Phase 6.4 — label 正则冒号**:`^([\w]+):` 返回 `lxnx0:`(带冒号),导致 `[mylabel=lxnx0:]` 找不到目标 - [x] **Phase 6.5 — CDATA 正则误匹配 clipactions**:原正则 `([^>]*)` 吞掉 `clipactions` 的 `s`,修复为 `(\s[^>]*)?` - [x] **Phase 6.6 — `&` 双编码**:`decodeEntities: false` 读取 raw XML 时 `&` 保持原样,cheerio 序列化再编码 → 需要 `decodeAttrEntities()` 手动解码 - [x] **Phase 6.7 — `pushconst "1"` 被误判为数字**:`parseInt("1")` = 1,`isNaN(1)` = false → 被当作数字常量。修复:检查原始 `argStr` 是否以引号开头 - [x] **Phase 6.8 — .const 文件整型标识符错误**:`writeUInt32LE(2)` 应为 `writeUInt32LE(7)`,并加 `console.warn` 警告 - [x] **Phase 6.9 — build 自动创建输出目录**:添加 `fs.mkdirSync` + `{ recursive: true }` 确保输出目录和 .ru 子目录自动创建 --- ## 4. 注意事项 ### 4.1 cheerio 使用要点 - 始终使用 `{ xmlMode: true, decodeEntities: false }` 加载 XML - 元素创建:`$('<' + tagName + '>')` — 创建的元素不会自动加入当前文档,需要手动 `.append()`/`.prepend()` 等 - `.text(text)` 设置文本时,`<` `&` 等字符自动 entity 编码,`.text()` 回读自动解码 - `.sort(fn)` 可用在 cheerio 集合上(继承 Array.prototype.sort) - `$(el).is(selector)` 只接受 CSS 选择器字符串,不支持传入 jQuery 对象(GUI 预览代码用 `$(f).is(curnode)` 但那是 GUI 专属,不需要移植) ### 4.2 二进制 .const 文件写入 第 237-257 行的 `Buffer.alloc` / `writeUInt32LE` / `asciiWrite` / `fs.openSync` / `fs.writeSync` / `fs.closeSync` 已经是**标准的 Node.js API,不需要额外改动**。注意: - 整型常量标识符从原代码的 `writeUInt32LE(2)` **修正为** `writeUInt32LE(7)`(7 才是正确的整型标识) - 当有整数写入 consttable 时会输出 `console.warn` 警告 ### 4.3 manifest XML 写回策略 - build 操作会写回 `filedir/[name].xml`(manifest XML),行为与 GUI 第 3089 行完全一致 - 同时也写回 `.dat`、`.ru` 文件 - CLI 不会写回 `_edit.xml` 本身,只消费它 ### 4.4 临时目录 - 遵循原代码方式(第 515 行):`fs.mkdtempSync(os.tmpdir() + path.sep) + path.sep` - apt raw XML (`[name].xml`) 生成在这里,处理完后清理 - 可通过 `--keep-temp` 标志保留临时目录用于调试 ### 4.5 cc3tools 路径 - 默认相对路径:`cc3tools/`(从启动位置出发,即与 `ra3apt-cli/` 同级的工作区目录或由用户指定) - 可通过 `--tools-path` 参数覆盖 ### 4.6 `pushconst` 字符串/数字区分 `pushconst "1"`(带引号)应作为**字符串常量**处理,`pushconst 1`(无引号)作为数字常量。区分逻辑: ```js // 检查原始 argStr 是否以引号开头 if (argStr && argStr[0] === '"') { a.attr('str', args[0] || ''); // 字符串 } else { const numVal = parseInt(args[0], 10); if (isNaN(numVal) || args[0] === '') { a.attr('str', args[0] || ''); // 不是数字 → 字符串 } else { a.attr('num', numVal); // 是数字 } } ``` ### 4.7 `decodeAttrEntities()` — 防止 `&` 双编码 expand 以 `decodeEntities: false` 加载 raw XML,属性值中的 `&` 保持原样。但 cheerio 序列化时会将文本中的 `&` 再编码为 `&`,导致双编码。修复:在 `actiontostring()` 中通过 `decodeAttrEntities()` 手动解码实体后再输出。 ```js function decodeAttrEntities(s) { if (typeof s !== 'string') return s; return s.replace(/&/g, '&').replace(/</g, '<') .replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'"); } ``` ### 4.8 自动创建输出目录 build 命令会在写入输出文件前自动创建目录: - `fs.mkdirSync(outDir, { recursive: true })` — 确保 `.apt`/`.const`/`.xml`/`.dat` 的输出目录存在 - `fs.mkdirSync(path.dirname(rup), { recursive: true })` — 确保 `.ru` 文件所在的子目录(如 `geometry/`)存在 用户无需手动创建输出目录,build 会递归创建所有缺失的父目录。 ### 4.9 错误时 return false 的处理 原始代码中 `xmlcleanhint()` 在出错时 `return false`,调用方据此判断。CLI 版本改为 `throw new Error()`,不再需要返回值检查。 原始模式(第 430 行): ```js if(xmlcleanhint()){ // 继续保存 }else{ // 保存失败 } ``` CLI 模式: ```js try { xmlcleanhint(...); // 出错时 throw // 继续保存 } catch(err) { console.error(err.message); process.exit(1); } ```