first commit

This commit is contained in:
2026-06-14 01:40:04 +02:00
commit 5511354783
4787 changed files with 103543 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
# 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);
}
```