Preserve edittext channels, alpha/color transforms, inactive placement fields, zero ratio/clipping and null ClipAction presence. Keep unlabelled flag bits, quoted/escaped script strings, export declarations and correctly typed CONST and pushdata values. Validate signed integers and write byte-aligned UTF-8. Add 23 public CLI regressions and record the native 174-resource batch plus bounded retests: four discovered pages are repaired, while the sound placeholder and native float formatting remain explicit accuracy limits. Rebuild the 1.0.1 Windows executable and leave unrelated SDK changes untouched.
14 KiB
Historical porting plan
The original phase checklist below describes version 1.0.0. Its sections 4.1, 4.2, 4.6 and 4.7 are superseded by the 1.0.1 single-decode, typed-operand and variable-length UTF-8 implementation. Use README.md, test/README.md and ROUNDTRIP_REPORT.md for current behavior and validation.
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 — 项目脚手架与常量提取 ✅
- 创建
ra3apt-cli/项目结构(已完成package.json初始化 +cheerio安装) - 安装
commander依赖 - 提取常量表到
lib/constants.js:ActionNames/ActionNames2(新旧两套 opcode 名称)ActionSizes/ActionAlignsPlaceObjectFlags/ClipEventFlags/ButtonRecordFlags/ButtonActionFlags/FunctionPreloadFlags/ButtonInput
- 创建 CLI 入口文件
bin/ra3apt.js,挂载expand和build子命令
Phase 2 — 工具函数层 ✅
- 提取通用工具函数到
lib/utils.js:endian()/valuetoflags()/flagstovalue()encodeXML()/mkstr()/el()/bad()/makelabel()alnsize()/calcaln()/thisaln()/nextaln()/copyindent()/indentstr()rgba()/getrgba()/cleanrgba()/getmatrix()/cleanmatrix()
- 适配浏览器 DOM 原生属性访问 → cheerio 等价写法
- 编写
expand/builder两流程共享的临时目录管理、exe 调用、文件复制逻辑
Phase 3 — 实现 ra3apt-cli expand ✅
- 实现
expand()流程:复制 .apt + .const → 运行apt2xml.exe→ 读取 manifest XML + .dat - 移植
xmladdhint()全部逻辑:- 常量展开
- 跳转 label 解析
- action 汇编化 + text node 写入
- flags 符号化
- image / shape 纹理解析
- 编辑者友好清理
- 移植
actiontostring()全部九种 action 分支 - 序列化输出人类可读 XML
Phase 4 — 实现 ra3apt-cli build ✅
- 移植
xmlcleanhint()全部逻辑:- 文本汇编解析 +
stringtoaction() - 常量池重构建 + .const 二进制写入(
BufferAPI,Node.js 原生) - character ID 重编号
- 跳转偏移量解析
- function size 计算
- frame 编号
- flags 数值化
- placeobject flags 构建
- shape/ru/image 处理 + manifest / .dat / .ru 生成
- 矩阵/颜色属性拆解和清理
- 文本汇编解析 +
- 运行
xml2apt.exe生成 .apt + .const - 写回 manifest XML、.dat、.ru 文件
Phase 5 — 端到端测试与文档 ✅
- 从
assets/获取 3 组测试数据(ig_HUD、fem_m_gameSetup、main_mouse) - 验证完整回路:
.apt→ expand →_edit.xml→ build →.apt→ apt2xml → raw XML 对比 - 全部 3 组测试通过,raw XML 完全一致
- 清理测试临时文件
- 编写 README 与 CLI 帮助文本
Phase 6 — Bug 修复与完善 ✅
- Phase 6.1 —
parseArgs引号保留:parseArgs()返回的参数字符串两端保留引号,导致str属性带多余引号 - Phase 6.2 — 标签名不匹配:build 产生
data/noarg标签但 cc3tools 需要byte/short/float/long - Phase 6.3 — placeobject flag 计算缺失 rotm00→matrix 转换:人类可读 XML 使用
rotm00格式,但 flag 计算需matrix格式 - Phase 6.4 — label 正则冒号:
^([\w]+):返回lxnx0:(带冒号),导致[mylabel=lxnx0:]找不到目标 - Phase 6.5 — CDATA 正则误匹配 clipactions:原正则
([^>]*)吞掉clipactions的s,修复为(\s[^>]*)? - Phase 6.6 —
&双编码:decodeEntities: false读取 raw XML 时&保持原样,cheerio 序列化再编码 → 需要decodeAttrEntities()手动解码 - Phase 6.7 —
pushconst "1"被误判为数字:parseInt("1")= 1,isNaN(1)= false → 被当作数字常量。修复:检查原始argStr是否以引号开头 - Phase 6.8 — .const 文件整型标识符错误:
writeUInt32LE(2)应为writeUInt32LE(7),并加console.warn警告 - 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(无引号)作为数字常量。区分逻辑:
// 检查原始 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() 手动解码实体后再输出。
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 行):
if(xmlcleanhint()){
// 继续保存
}else{
// 保存失败
}
CLI 模式:
try {
xmlcleanhint(...); // 出错时 throw
// 继续保存
} catch(err) {
console.error(err.message);
process.exit(1);
}