first commit
This commit is contained in:
@@ -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`(复数)不会被误匹配,因为正则要求标签名完整后跟空格或 `>`
|
||||
- `&` 双编码问题通过 `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` 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);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user