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

13 KiB
Raw Permalink Blame History

RA3 Apt CLI — 工作计划

将 ra3aptxmleditor 的 .apt ↔ 人类可读 XML 双向转换流程 CLI 化。 基于 ra3apt-cli/ 目录下的 Node.js 项目。


0. 项目渊源

本 CLI 工具基于两个上游项目:

  • jonwil/cc3tools — 提供 apt2xml.exe.apt → raw XML)和 xml2apt.exeraw 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)
  • 仅 Windowscc3tools 提供的是 Windows .exe

1.2 输出文件命名

操作 默认输出 说明
expand <name>_edit.xml 人类可读 XML
build <name>.apt + <name>.const 二进制 .apt + 常量表

用户可自由指定 -o 覆盖默认名。

1.3 文件冲突策略

人类可读 XML 与 manifest XML 不会冲突,因为:

  • manifest XML 是源目录中的 [name].xmlAssetDeclaration 格式,由 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.prevcheerio 节点) 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 / ActionAligns
    • PlaceObjectFlags / ClipEventFlags / ButtonRecordFlags / ButtonActionFlags / FunctionPreloadFlags / ButtonInput
  • 创建 CLI 入口文件 bin/ra3apt.js,挂载 expandbuild 子命令

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 二进制写入(Buffer APINode.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:原正则 ([^>]*) 吞掉 clipactionss,修复为 (\s[^>]*)?
  • Phase 6.6 — &amp; 双编码decodeEntities: false 读取 raw XML 时 &amp; 保持原样,cheerio 序列化再编码 → 需要 decodeAttrEntities() 手动解码
  • Phase 6.7 — pushconst "1" 被误判为数字parseInt("1") = 1isNaN(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].xmlmanifest 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() — 防止 &amp; 双编码

expand 以 decodeEntities: false 加载 raw XML,属性值中的 &amp; 保持原样。但 cheerio 序列化时会将文本中的 & 再编码为 &amp;,导致双编码。修复:在 actiontostring() 中通过 decodeAttrEntities() 手动解码实体后再输出。

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 行):

if(xmlcleanhint()){
  // 继续保存
}else{
  // 保存失败
}

CLI 模式:

try {
  xmlcleanhint(...);  // 出错时 throw
  // 继续保存
} catch(err) {
  console.error(err.message);
  process.exit(1);
}