Files
2026-06-14 01:40:04 +02:00

287 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | `<name>_edit.xml` | 人类可读 XML |
| `build` | `<name>.apt` + `<name>.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 的 `<shape><ru>` 元素中,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 节点,所以用正则后处理实现
- 正则必须匹配带属性的标签(如 `<clipaction flags="Construct">`
- `clipactions`(复数)不会被误匹配,因为正则要求标签名完整后跟空格或 `>`
- `&amp;` 双编码问题通过 `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)` | 正则后处理替换:`/<tag>([\s\S]*?)<\/tag>/``<tag><![CDATA[$1]]></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` APINode.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 — `&amp;` 双编码**`decodeEntities: false` 读取 raw XML 时 `&amp;` 保持原样,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()` — 防止 `&amp;` 双编码
expand 以 `decodeEntities: false` 加载 raw XML,属性值中的 `&amp;` 保持原样。但 cheerio 序列化时会将文本中的 `&` 再编码为 `&amp;`,导致双编码。修复:在 `actiontostring()` 中通过 `decodeAttrEntities()` 手动解码实体后再输出。
```js
function decodeAttrEntities(s) {
if (typeof s !== 'string') return s;
return s.replace(/&amp;/g, '&').replace(/&lt;/g, '<')
.replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/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);
}
```