first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
@@ -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);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { program } = require('commander');
|
||||
const path = require('path');
|
||||
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// ─── Shared helper: resolve cc3tools path ───
|
||||
function resolveToolsDir(options) {
|
||||
if (options.toolsPath) {
|
||||
return path.resolve(options.toolsPath) + path.sep;
|
||||
}
|
||||
// If running as a packaged executable
|
||||
if (process.pkg) {
|
||||
return path.join(path.dirname(process.execPath), 'cc3tools') + path.sep;
|
||||
}
|
||||
// Default: ../cc3tools/ relative to this script's location
|
||||
return path.resolve(__dirname, '..', 'cc3tools') + path.sep;
|
||||
}
|
||||
|
||||
// ─── ra3apt expand ───
|
||||
program
|
||||
.command('expand')
|
||||
.description('Convert a binary .apt file to human-readable XML')
|
||||
.argument('<input>', 'Path to the .apt file')
|
||||
.option('-o, --output <file>', 'Output XML file (default: <name>_edit.xml)')
|
||||
.option('--tools-path <dir>', 'Path to cc3tools directory')
|
||||
.option('--keep-temp', 'Keep temporary files for debugging')
|
||||
.option('--old-style', 'Use old/verbose action name style')
|
||||
.action(async (input, options) => {
|
||||
const toolsDir = resolveToolsDir(options);
|
||||
const { expand } = require('../lib/expand');
|
||||
try {
|
||||
await expand(input, options.output, {
|
||||
toolsDir,
|
||||
keepTemp: !!options.keepTemp,
|
||||
oldStyle: !!options.oldStyle
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── ra3apt build ───
|
||||
program
|
||||
.command('build')
|
||||
.description('Build a binary .apt file from human-readable XML')
|
||||
.argument('<input>', 'Path to the human-readable XML file')
|
||||
.option('-o, --output <file>', 'Output .apt file (default: <name>.apt)')
|
||||
.option('--tools-path <dir>', 'Path to cc3tools directory')
|
||||
.option('--keep-temp', 'Keep temporary files for debugging')
|
||||
.option('--old-style', 'Use old/verbose action name style')
|
||||
.action(async (input, options) => {
|
||||
const toolsDir = resolveToolsDir(options);
|
||||
const { build } = require('../lib/build');
|
||||
try {
|
||||
await build(input, options.output, {
|
||||
toolsDir,
|
||||
keepTemp: !!options.keepTemp,
|
||||
oldStyle: !!options.oldStyle
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Parse ───
|
||||
program
|
||||
.name('ra3apt')
|
||||
.description('RA3 .apt file editor CLI — convert between binary .apt and human-readable XML.\nBased on Jonwil\'s cc3tools and utunnels\' ra3aptxmleditor.')
|
||||
.version(pkg.version);
|
||||
|
||||
program.parse(process.argv);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,886 @@
|
||||
'use strict';
|
||||
|
||||
// ─── Bytecode action opcodes (hex values) ───
|
||||
const ACTION_END = 0x00;
|
||||
const ACTION_NEXTFRAME = 0x04;
|
||||
const ACTION_PREVFRAME = 0x05;
|
||||
const ACTION_PLAY = 0x06;
|
||||
const ACTION_STOP = 0x07;
|
||||
const ACTION_TOGGLEQUALITY = 0x08;
|
||||
const ACTION_STOPSOUNDS = 0x09;
|
||||
const ACTION_ADD = 0x0A;
|
||||
const ACTION_SUBTRACT = 0x0B;
|
||||
const ACTION_MULTIPLY = 0x0C;
|
||||
const ACTION_DIVIDE = 0x0D;
|
||||
const ACTION_EQUAL = 0x0E;
|
||||
const ACTION_LESSTHAN = 0x0F;
|
||||
const ACTION_LOGICALAND = 0x10;
|
||||
const ACTION_LOGICALOR = 0x11;
|
||||
const ACTION_LOGICALNOT = 0x12;
|
||||
const ACTION_STRINGEQ = 0x13;
|
||||
const ACTION_STRINGLENGTH = 0x14;
|
||||
const ACTION_SUBSTRING = 0x15;
|
||||
const ACTION_POP = 0x17;
|
||||
const ACTION_INT = 0x18;
|
||||
const ACTION_GETVARIABLE = 0x1C;
|
||||
const ACTION_SETVARIABLE = 0x1D;
|
||||
const ACTION_SETTARGETEXPRESSION = 0x20;
|
||||
const ACTION_STRINGCONCAT = 0x21;
|
||||
const ACTION_GETPROPERTY = 0x22;
|
||||
const ACTION_SETPROPERTY = 0x23;
|
||||
const ACTION_DUPLICATECLIP = 0x24;
|
||||
const ACTION_REMOVECLIP = 0x25;
|
||||
const ACTION_TRACE = 0x26;
|
||||
const ACTION_STARTDRAGMOVIE = 0x27;
|
||||
const ACTION_STOPDRAGMOVIE = 0x28;
|
||||
const ACTION_STRINGCOMPARE = 0x29;
|
||||
const ACTION_THROW = 0x2A;
|
||||
const ACTION_CASTOP = 0x2B;
|
||||
const ACTION_IMPLEMENTSOP = 0x2C;
|
||||
const ACTION_RANDOM = 0x30;
|
||||
const ACTION_MBLENGTH = 0x31;
|
||||
const ACTION_ORD = 0x32;
|
||||
const ACTION_CHR = 0x33;
|
||||
const ACTION_GETTIMER = 0x34;
|
||||
const ACTION_MBSUBSTRING = 0x35;
|
||||
const ACTION_MBORD = 0x36;
|
||||
const ACTION_MBCHR = 0x37;
|
||||
const ACTION_DELETE = 0x3A;
|
||||
const ACTION_DELETE2 = 0x3B;
|
||||
const ACTION_DEFINELOCAL = 0x3C;
|
||||
const ACTION_CALLFUNCTION = 0x3D;
|
||||
const ACTION_RETURN = 0x3E;
|
||||
const ACTION_MODULO = 0x3F;
|
||||
const ACTION_NEW = 0x40;
|
||||
const ACTION_VAR = 0x41;
|
||||
const ACTION_INITARRAY = 0x42;
|
||||
const ACTION_INITOBJECT = 0x43;
|
||||
const ACTION_TYPEOF = 0x44;
|
||||
const ACTION_TARGETPATH = 0x45;
|
||||
const ACTION_ENUMERATE = 0x46;
|
||||
const ACTION_NEWADD = 0x47;
|
||||
const ACTION_NEWLESSTHAN = 0x48;
|
||||
const ACTION_NEWEQUALS = 0x49;
|
||||
const ACTION_TONUMBER = 0x4A;
|
||||
const ACTION_TOSTRING = 0x4B;
|
||||
const ACTION_DUP = 0x4C;
|
||||
const ACTION_SWAP = 0x4D;
|
||||
const ACTION_GETMEMBER = 0x4E;
|
||||
const ACTION_SETMEMBER = 0x4F;
|
||||
const ACTION_INCREMENT = 0x50;
|
||||
const ACTION_DECREMENT = 0x51;
|
||||
const ACTION_CALLMETHOD = 0x52;
|
||||
const ACTION_NEWMETHOD = 0x53;
|
||||
const ACTION_INSTANCEOF = 0x54;
|
||||
const ACTION_ENUM2 = 0x55;
|
||||
const ACTION_BITWISEAND = 0x60;
|
||||
const ACTION_BITWISEOR = 0x61;
|
||||
const ACTION_BITWISEXOR = 0x62;
|
||||
const ACTION_SHIFTLEFT = 0x63;
|
||||
const ACTION_SHIFTRIGHT = 0x64;
|
||||
const ACTION_SHIFTRIGHT2 = 0x65;
|
||||
const ACTION_STRICTEQ = 0x66;
|
||||
const ACTION_GREATER = 0x67;
|
||||
const ACTION_STRINGGREATER = 0x68;
|
||||
const ACTION_EXTENDS = 0x69;
|
||||
const ACTION_WAITFORFRAME = 0x8A;
|
||||
const ACTION_WAITFORFRAMEEXPRESSION = 0x8D;
|
||||
const ACTION_SETREGISTER = 0x87;
|
||||
const ACTION_CONSTANTPOOL = 0x88;
|
||||
const ACTION_PUSHDATA = 0x96;
|
||||
const ACTION_BRANCHALWAYS = 0x99;
|
||||
const ACTION_GETURL2 = 0x9A;
|
||||
const ACTION_DEFINEFUNCTION = 0x9B;
|
||||
const ACTION_BRANCHIFTRUE = 0x9D;
|
||||
const ACTION_CALLFRAME = 0x9E;
|
||||
const ACTION_GOTOEXPRESSION = 0x9F;
|
||||
const ACTION_GOTOFRAME = 0x81;
|
||||
const ACTION_GETURL = 0x83;
|
||||
const ACTION_SETTARGET = 0x8B;
|
||||
const ACTION_GOTOLABEL = 0x8C;
|
||||
const ACTION_DEFINEFUNCTION2 = 0x8E;
|
||||
const ACTION_TRY = 0x8F;
|
||||
const ACTION_WITH = 0x94;
|
||||
|
||||
// EA-specific opcodes
|
||||
const EA_ACTION56 = 0x56;
|
||||
const EA_ACTION58 = 0x58;
|
||||
const EA_PUSHZERO = 0x59;
|
||||
const EA_PUSHONE = 0x5A;
|
||||
const EA_CALLFUNCTIONPOP = 0x5B;
|
||||
const EA_CALLFUNCTION = 0x5C;
|
||||
const EA_CALLMETHODPOP = 0x5D;
|
||||
const EA_CALLMETHOD = 0x5E;
|
||||
const EA_PUSHTHIS = 0x70;
|
||||
const EA_PUSHGLOBAL = 0x71;
|
||||
const EA_ZEROVARIABLE = 0x72;
|
||||
const EA_PUSHTRUE = 0x73;
|
||||
const EA_PUSHFALSE = 0x74;
|
||||
const EA_PUSHNULL = 0x75;
|
||||
const EA_PUSHUNDEFINED = 0x76;
|
||||
const EA_ACTION77 = 0x77;
|
||||
const EA_PUSHSTRING = 0xA1;
|
||||
const EA_PUSHCONSTANT = 0xA2;
|
||||
const EA_PUSHWORDCONSTANT = 0xA3;
|
||||
const EA_GETSTRINGVAR = 0xA4;
|
||||
const EA_GETSTRINGMEMBER = 0xA5;
|
||||
const EA_SETSTRINGVAR = 0xA6;
|
||||
const EA_SETSTRINGMEMBER = 0xA7;
|
||||
const EA_PUSHVALUEOFVAR = 0xAE;
|
||||
const EA_GETNAMEDMEMBER = 0xAF;
|
||||
const EA_CALLNAMEDFUNCTIONPOP = 0xB0;
|
||||
const EA_CALLNAMEDFUNCTION = 0xB1;
|
||||
const EA_CALLNAMEDMETHODPOP = 0xB2;
|
||||
const EA_CALLNAMEDMETHOD = 0xB3;
|
||||
const EA_PUSHFLOAT = 0xB4;
|
||||
const EA_PUSHBYTE = 0xB5;
|
||||
const EA_PUSHSHORT = 0xB6;
|
||||
const EA_PUSHLONG = 0xB7;
|
||||
const EA_BRANCHIFFALSE = 0xB8;
|
||||
const EA_PUSHREGISTER = 0xB9;
|
||||
|
||||
// ─── Flag enums ───
|
||||
|
||||
const PlaceObjectFlags = {
|
||||
Move: 1,
|
||||
HasCharacter: 2,
|
||||
HasMatrix: 4,
|
||||
HasColorTransform: 8,
|
||||
HasRatio: 16,
|
||||
HasName: 32,
|
||||
HasClipDepth: 64,
|
||||
HasClipAction: 128
|
||||
};
|
||||
|
||||
const ClipEventFlags = {
|
||||
KeyUp: 0x800000,
|
||||
KeyDown: 0x400000,
|
||||
MouseUp: 0x200000,
|
||||
MouseDown: 0x100000,
|
||||
MouseMove: 0x080000,
|
||||
Unload: 0x040000,
|
||||
EnterFrame: 0x020000,
|
||||
Load: 0x010000,
|
||||
DragOver: 0x008000,
|
||||
RollOut: 0x004000,
|
||||
RollOver: 0x002000,
|
||||
ReleaseOutside: 0x001000,
|
||||
Release: 0x000800,
|
||||
Press: 0x000400,
|
||||
DragOut: 0x000200,
|
||||
Data: 0x000100,
|
||||
Construct: 0x000004,
|
||||
KeyPress: 0x000002,
|
||||
Initialize: 0x000001
|
||||
};
|
||||
|
||||
const ButtonRecordFlags = {
|
||||
None: 0,
|
||||
StateUp: 1,
|
||||
StateOver: 2,
|
||||
StateDown: 4,
|
||||
StateHit: 8
|
||||
};
|
||||
|
||||
const ButtonActionFlags = {
|
||||
IdleToOverUp: 1,
|
||||
OverUpToIdle: 2,
|
||||
OverUpToOverDown: 4,
|
||||
OverDownToOverUp: 8,
|
||||
OverDownToOutDown: 16,
|
||||
OutDownToOverDown: 32,
|
||||
IdleToOverDown: 128,
|
||||
OverDownToIdle: 256
|
||||
};
|
||||
|
||||
const ButtonInput = {
|
||||
MouseButton0: 0,
|
||||
Left: 1,
|
||||
Right: 2,
|
||||
Home: 3,
|
||||
End: 4,
|
||||
Insert: 5,
|
||||
Delete: 6,
|
||||
BackSpace: 8,
|
||||
Enter: 13,
|
||||
Unknown: 240
|
||||
};
|
||||
|
||||
const FunctionPreloadFlags = {
|
||||
PreloadExtern: 0x010000,
|
||||
PreloadParent: 0x008000,
|
||||
PreloadRoot: 0x004000,
|
||||
SupressSuper: 0x002000,
|
||||
PreloadSuper: 0x001000,
|
||||
SupressArguments: 0x000800,
|
||||
PreloadArguments: 0x000400,
|
||||
SupressThis: 0x000200,
|
||||
PreloadThis: 0x000100,
|
||||
PreloadGlobal: 0x000001
|
||||
};
|
||||
|
||||
// ─── Action name tables ───
|
||||
// New/shorter style names (default)
|
||||
const ActionNames = [
|
||||
"actionEnd", // 0x00
|
||||
"", // 0x01
|
||||
"", // 0x02
|
||||
"", // 0x03
|
||||
"ActionNextFrame", // 0x04
|
||||
"ActionPrevFrame", // 0x05
|
||||
"play", // 0x06
|
||||
"stop", // 0x07
|
||||
"ActionToggleQuality", // 0x08
|
||||
"ActionStopSounds", // 0x09
|
||||
"ActionAdd", // 0x0A
|
||||
"ActionSubtract", // 0x0B
|
||||
"ActionMultiply", // 0x0C
|
||||
"ActionDivide", // 0x0D
|
||||
"ActionEqual", // 0x0E
|
||||
"ActionLessThan", // 0x0F
|
||||
"ActionLogicalAnd", // 0x10
|
||||
"ActionLogicalOr", // 0x11
|
||||
"ActionLogicalNot", // 0x12
|
||||
"ActionStringEq", // 0x13
|
||||
"ActionStringLength", // 0x14
|
||||
"ActionSubString", // 0x15
|
||||
"", // 0x16
|
||||
"ActionPop", // 0x17
|
||||
"ActionInt", // 0x18
|
||||
"", // 0x19
|
||||
"", // 0x1A
|
||||
"", // 0x1B
|
||||
"ActionGetVariable", // 0x1C
|
||||
"ActionSetVariable", // 0x1D
|
||||
"", // 0x1E
|
||||
"", // 0x1F
|
||||
"ActionSetTargetExpression", // 0x20
|
||||
"ActionStringConcat", // 0x21
|
||||
"ActionGetProperty", // 0x22
|
||||
"ActionSetProperty", // 0x23
|
||||
"ActionDuplicateClip", // 0x24
|
||||
"ActionRemoveClip", // 0x25
|
||||
"ActionTrace", // 0x26
|
||||
"ActionStartDrag", // 0x27
|
||||
"ActionStopDrag", // 0x28
|
||||
"ActionStringCompare", // 0x29
|
||||
"ActionThrow", // 0x2A
|
||||
"ActionCastOp", // 0x2B
|
||||
"ActionImplementsOp", // 0x2C
|
||||
"", // 0x2D
|
||||
"", // 0x2E
|
||||
"", // 0x2F
|
||||
"ActionRandom", // 0x30
|
||||
"ActionMBLength", // 0x31
|
||||
"ActionOrd", // 0x32
|
||||
"ActionChr", // 0x33
|
||||
"ActionGetTimer", // 0x34
|
||||
"ActionMBSubString", // 0x35
|
||||
"ActionMBOrd", // 0x36
|
||||
"ActionMBChr", // 0x37
|
||||
"", // 0x38
|
||||
"", // 0x39
|
||||
"ActionDelete", // 0x3A
|
||||
"ActionDelete2", // 0x3B
|
||||
"ActionDefineLocal", // 0x3C
|
||||
"ActionCallFunction", // 0x3D
|
||||
"ActionReturn", // 0x3E
|
||||
"ActionModulo", // 0x3F
|
||||
"ActionNew", // 0x40
|
||||
"ActionVar", // 0x41
|
||||
"ActionInitArray", // 0x42
|
||||
"ActionInitObject", // 0x43
|
||||
"ActionTypeOf", // 0x44
|
||||
"ActionTargetPath", // 0x45
|
||||
"ActionEnumerate", // 0x46
|
||||
"ActionNewAdd", // 0x47
|
||||
"ActionNewLessThan", // 0x48
|
||||
"ActionNewEquals", // 0x49
|
||||
"ActionToNumber", // 0x4A
|
||||
"ActionToString", // 0x4B
|
||||
"ActionDup", // 0x4C
|
||||
"ActionSwap", // 0x4D
|
||||
"ActionGetMember", // 0x4E
|
||||
"ActionSetMember", // 0x4F
|
||||
"ActionIncrement", // 0x50
|
||||
"ActionDecrement", // 0x51
|
||||
"ActionCallMethod", // 0x52
|
||||
"ActionNewMethod", // 0x53
|
||||
"ActionInstanceOf", // 0x54
|
||||
"ActionEnum2", // 0x55
|
||||
"EAUnknownAction56", // 0x56
|
||||
"", // 0x57
|
||||
"EAUnknownAction58", // 0x58
|
||||
"EAPushZero", // 0x59
|
||||
"EAPushOne", // 0x5A
|
||||
"EACallFunctionPop", // 0x5B
|
||||
"callset", // 0x5C
|
||||
"callmethodpop", // 0x5D
|
||||
"callmethodset", // 0x5E
|
||||
"", // 0x5F
|
||||
"bitand", // 0x60
|
||||
"bitor", // 0x61
|
||||
"xor", // 0x62
|
||||
"lsh", // 0x63
|
||||
"rsh", // 0x64
|
||||
"unsignedrsh", // 0x65
|
||||
"stricteq", // 0x66
|
||||
"gt", // 0x67
|
||||
"strgt", // 0x68
|
||||
"extends", // 0x69
|
||||
"", // 0x6A
|
||||
"", // 0x6B
|
||||
"", // 0x6C
|
||||
"", // 0x6D
|
||||
"", // 0x6E
|
||||
"", // 0x6F
|
||||
"pushthis", // 0x70
|
||||
"pushglobal", // 0x71
|
||||
"zerovar", // 0x72
|
||||
"pushtrue", // 0x73
|
||||
"pushfalse", // 0x74
|
||||
"pushnull", // 0x75
|
||||
"pushundefined", // 0x76
|
||||
"", // 0x77
|
||||
"", // 0x78
|
||||
"", // 0x79
|
||||
"", // 0x7A
|
||||
"", // 0x7B
|
||||
"", // 0x7C
|
||||
"", // 0x7D
|
||||
"", // 0x7E
|
||||
"", // 0x7F
|
||||
"", // 0x80
|
||||
"gotoframe", // 0x81
|
||||
"", // 0x82
|
||||
"geturl", // 0x83
|
||||
"", // 0x84
|
||||
"", // 0x85
|
||||
"", // 0x86
|
||||
"setreg", // 0x87
|
||||
"ActionConstantPool", // 0x88
|
||||
"", // 0x89
|
||||
"waitforframe", // 0x8A
|
||||
"settarget", // 0x8B
|
||||
"gotolabel", // 0x8C
|
||||
"waitforframeexpr", // 0x8D
|
||||
"ActionDefineFunction2", // 0x8E
|
||||
"try", // 0x8F
|
||||
"", // 0x90
|
||||
"", // 0x91
|
||||
"", // 0x92
|
||||
"", // 0x93
|
||||
"with", // 0x94
|
||||
"", // 0x95
|
||||
"pushdata", // 0x96
|
||||
"", // 0x97
|
||||
"", // 0x98
|
||||
"jump", // 0x99
|
||||
"geturl2", // 0x9A
|
||||
"ActionDefineFunction", // 0x9B
|
||||
"", // 0x9C
|
||||
"jumptrue", // 0x9D
|
||||
"callframe", // 0x9E
|
||||
"gotoexpr", // 0x9F
|
||||
"", // 0xA0
|
||||
"pushstr", // 0xA1
|
||||
"pushconst", // 0xA2
|
||||
"pushwconst", // 0xA3
|
||||
"getstrvar", // 0xA4
|
||||
"getstrmember", // 0xA5
|
||||
"setstrvar", // 0xA6
|
||||
"setstrmember", // 0xA7
|
||||
"", // 0xA8
|
||||
"", // 0xA9
|
||||
"", // 0xAA
|
||||
"", // 0xAB
|
||||
"", // 0xAC
|
||||
"", // 0xAD
|
||||
"pushvar", // 0xAE
|
||||
"getnamedmember", // 0xAF
|
||||
"callnamedpop", // 0xB0
|
||||
"callnamedset", // 0xB1
|
||||
"callnamedmethodpop", // 0xB2
|
||||
"callnamedmethodset", // 0xB3
|
||||
"pushfloat", // 0xB4
|
||||
"pushbyte", // 0xB5
|
||||
"pushshort", // 0xB6
|
||||
"pushlong", // 0xB7
|
||||
"jumpfalse", // 0xB8
|
||||
"pushreg" // 0xB9
|
||||
];
|
||||
|
||||
// Old/verbose style names
|
||||
const ActionNames2 = [
|
||||
"ActionEnd", // 0x00
|
||||
"", // 0x01
|
||||
"", // 0x02
|
||||
"", // 0x03
|
||||
"ActionNextFrame", // 0x04
|
||||
"ActionPrevFrame", // 0x05
|
||||
"ActionPlay", // 0x06
|
||||
"ActionStop", // 0x07
|
||||
"ActionToggleQuality", // 0x08
|
||||
"ActionStopSounds", // 0x09
|
||||
"ActionAdd", // 0x0A
|
||||
"ActionSubtract", // 0x0B
|
||||
"ActionMultiply", // 0x0C
|
||||
"ActionDivide", // 0x0D
|
||||
"ActionEqual", // 0x0E
|
||||
"ActionLessThan", // 0x0F
|
||||
"ActionLogicalAnd", // 0x10
|
||||
"ActionLogicalOr", // 0x11
|
||||
"ActionLogicalNot", // 0x12
|
||||
"ActionStringEq", // 0x13
|
||||
"ActionStringLength", // 0x14
|
||||
"ActionSubString", // 0x15
|
||||
"", // 0x16
|
||||
"ActionPop", // 0x17
|
||||
"ActionInt", // 0x18
|
||||
"", // 0x19
|
||||
"", // 0x1A
|
||||
"", // 0x1B
|
||||
"ActionGetVariable", // 0x1C
|
||||
"ActionSetVariable", // 0x1D
|
||||
"", // 0x1E
|
||||
"", // 0x1F
|
||||
"ActionSetTargetExpression", // 0x20
|
||||
"ActionStringConcat", // 0x21
|
||||
"ActionGetProperty", // 0x22
|
||||
"ActionSetProperty", // 0x23
|
||||
"ActionDuplicateClip", // 0x24
|
||||
"ActionRemoveClip", // 0x25
|
||||
"ActionTrace", // 0x26
|
||||
"ActionStartDrag", // 0x27
|
||||
"ActionStopDrag", // 0x28
|
||||
"ActionStringCompare", // 0x29
|
||||
"ActionThrow", // 0x2A
|
||||
"ActionCastOp", // 0x2B
|
||||
"ActionImplementsOp", // 0x2C
|
||||
"", // 0x2D
|
||||
"", // 0x2E
|
||||
"", // 0x2F
|
||||
"ActionRandom", // 0x30
|
||||
"ActionMBLength", // 0x31
|
||||
"ActionOrd", // 0x32
|
||||
"ActionChr", // 0x33
|
||||
"ActionGetTimer", // 0x34
|
||||
"ActionMBSubString", // 0x35
|
||||
"ActionMBOrd", // 0x36
|
||||
"ActionMBChr", // 0x37
|
||||
"", // 0x38
|
||||
"", // 0x39
|
||||
"ActionDelete", // 0x3A
|
||||
"ActionDelete2", // 0x3B
|
||||
"ActionDefineLocal", // 0x3C
|
||||
"ActionCallFunction", // 0x3D
|
||||
"ActionReturn", // 0x3E
|
||||
"ActionModulo", // 0x3F
|
||||
"ActionNew", // 0x40
|
||||
"ActionVar", // 0x41
|
||||
"ActionInitArray", // 0x42
|
||||
"ActionInitObject", // 0x43
|
||||
"ActionTypeOf", // 0x44
|
||||
"ActionTargetPath", // 0x45
|
||||
"ActionEnumerate", // 0x46
|
||||
"ActionNewAdd", // 0x47
|
||||
"ActionNewLessThan", // 0x48
|
||||
"ActionNewEquals", // 0x49
|
||||
"ActionToNumber", // 0x4A
|
||||
"ActionToString", // 0x4B
|
||||
"ActionDup", // 0x4C
|
||||
"ActionSwap", // 0x4D
|
||||
"ActionGetMember", // 0x4E
|
||||
"ActionSetMember", // 0x4F
|
||||
"ActionIncrement", // 0x50
|
||||
"ActionDecrement", // 0x51
|
||||
"ActionCallMethod", // 0x52
|
||||
"ActionNewMethod", // 0x53
|
||||
"ActionInstanceOf", // 0x54
|
||||
"ActionEnum2", // 0x55
|
||||
"EAUnknownAction56", // 0x56
|
||||
"", // 0x57
|
||||
"EAUnknownAction58", // 0x58
|
||||
"EAPushZero", // 0x59
|
||||
"EAPushOne", // 0x5A
|
||||
"EACallFunctionPop", // 0x5B
|
||||
"EACallFunction", // 0x5C
|
||||
"EACAllMethodPop", // 0x5D
|
||||
"EACallMethod", // 0x5E
|
||||
"", // 0x5F
|
||||
"ActionBitwiseAnd", // 0x60
|
||||
"ActionBitwiseOr", // 0x61
|
||||
"ActionBitwiseXor", // 0x62
|
||||
"ActionShiftLeft", // 0x63
|
||||
"ActionShiftRight", // 0x64
|
||||
"ActionShiftRight2", // 0x65
|
||||
"ActionStrictEq", // 0x66
|
||||
"ActionGreater", // 0x67
|
||||
"ActionStringGreater", // 0x68
|
||||
"ActionExtends", // 0x69
|
||||
"", // 0x6A
|
||||
"", // 0x6B
|
||||
"", // 0x6C
|
||||
"", // 0x6D
|
||||
"", // 0x6E
|
||||
"", // 0x6F
|
||||
"EAPushThis", // 0x70
|
||||
"EAPushGlobal", // 0x71
|
||||
"EAZeroVariable", // 0x72
|
||||
"EAPushTrue", // 0x73
|
||||
"EAPushFalse", // 0x74
|
||||
"EAPushNull", // 0x75
|
||||
"EAPushUndefined", // 0x76
|
||||
"", // 0x77
|
||||
"", // 0x78
|
||||
"", // 0x79
|
||||
"", // 0x7A
|
||||
"", // 0x7B
|
||||
"", // 0x7C
|
||||
"", // 0x7D
|
||||
"", // 0x7E
|
||||
"", // 0x7F
|
||||
"", // 0x80
|
||||
"ActionGotoFrame", // 0x81
|
||||
"", // 0x82
|
||||
"ActionGetURL", // 0x83
|
||||
"", // 0x84
|
||||
"", // 0x85
|
||||
"", // 0x86
|
||||
"ActionSetRegister", // 0x87
|
||||
"ActionConstantPool", // 0x88
|
||||
"", // 0x89
|
||||
"ActionWaitForFrame", // 0x8A
|
||||
"ActionSetTarget", // 0x8B
|
||||
"ActionGotoLabel", // 0x8C
|
||||
"ActionWaitForFrameExpression", // 0x8D
|
||||
"ActionDefineFunction2", // 0x8E
|
||||
"ActionTry", // 0x8F
|
||||
"", // 0x90
|
||||
"", // 0x91
|
||||
"", // 0x92
|
||||
"", // 0x93
|
||||
"ActionWith", // 0x94
|
||||
"", // 0x95
|
||||
"ActionPushData", // 0x96
|
||||
"", // 0x97
|
||||
"", // 0x98
|
||||
"ActionBranchAlways", // 0x99
|
||||
"ActionGetURL2", // 0x9A
|
||||
"ActionDefineFunction", // 0x9B
|
||||
"", // 0x9C
|
||||
"ActionBranchIfTrue", // 0x9D
|
||||
"ActionCallFrame", // 0x9E
|
||||
"ActionGotoExpression", // 0x9F
|
||||
"", // 0xA0
|
||||
"EAPushString", // 0xA1
|
||||
"EAPushConstant", // 0xA2
|
||||
"EAPushWordConstant", // 0xA3
|
||||
"EAGetStringVar", // 0xA4
|
||||
"EAGetStringMember", // 0xA5
|
||||
"EASetStringVar", // 0xA6
|
||||
"EASetStringMember", // 0xA7
|
||||
"", // 0xA8
|
||||
"", // 0xA9
|
||||
"", // 0xAA
|
||||
"", // 0xAB
|
||||
"", // 0xAC
|
||||
"", // 0xAD
|
||||
"EAPushValueOfVar", // 0xAE
|
||||
"EAGetNamedMember", // 0xAF
|
||||
"EACallNamedFunctionPop", // 0xB0
|
||||
"EACallNamedFunction", // 0xB1
|
||||
"EACallNamedMethodPop", // 0xB2
|
||||
"EACallNamedMethod", // 0xB3
|
||||
"EAPushFloat", // 0xB4
|
||||
"EAPushByte", // 0xB5
|
||||
"EAPushShort", // 0xB6
|
||||
"EAPushLong", // 0xB7
|
||||
"EABranchIfFalse", // 0xB8
|
||||
"EAPushRegister" // 0xB9
|
||||
];
|
||||
|
||||
// ─── Action alignment table ───
|
||||
// 1 = this action's body needs alignment to 4-byte boundary
|
||||
const ActionAligns = new Array(0xB9 + 1).fill(0);
|
||||
ActionAligns[ACTION_BRANCHALWAYS] = 1;
|
||||
ActionAligns[ACTION_BRANCHIFTRUE] = 1;
|
||||
ActionAligns[EA_BRANCHIFFALSE] = 1;
|
||||
ActionAligns[ACTION_GOTOFRAME] = 1;
|
||||
ActionAligns[ACTION_SETREGISTER] = 1;
|
||||
ActionAligns[ACTION_WITH] = 1;
|
||||
ActionAligns[ACTION_GOTOEXPRESSION] = 1;
|
||||
ActionAligns[ACTION_GETURL] = 1;
|
||||
ActionAligns[ACTION_CONSTANTPOOL] = 1;
|
||||
ActionAligns[ACTION_PUSHDATA] = 1;
|
||||
ActionAligns[ACTION_DEFINEFUNCTION2] = 1;
|
||||
ActionAligns[ACTION_DEFINEFUNCTION] = 1;
|
||||
ActionAligns[EA_PUSHSTRING] = 1;
|
||||
ActionAligns[EA_GETSTRINGVAR] = 1;
|
||||
ActionAligns[EA_GETSTRINGMEMBER] = 1;
|
||||
ActionAligns[EA_SETSTRINGVAR] = 1;
|
||||
ActionAligns[EA_SETSTRINGMEMBER] = 1;
|
||||
ActionAligns[ACTION_SETTARGET] = 1;
|
||||
ActionAligns[ACTION_GOTOLABEL] = 1;
|
||||
|
||||
// ─── Action size table ───
|
||||
// Fixed byte size of each action (0 = unknown/empty, 1 = no additional data)
|
||||
const ActionSizes = [
|
||||
1, // ActionEnd 0x00
|
||||
0, // (empty) 0x01
|
||||
0, // (empty) 0x02
|
||||
0, // (empty) 0x03
|
||||
1, // ActionNextFrame 0x04
|
||||
1, // ActionPrevFrame 0x05
|
||||
1, // ActionPlay 0x06
|
||||
1, // ActionStop 0x07
|
||||
1, // ActionToggleQuality 0x08
|
||||
1, // ActionStopSounds 0x09
|
||||
1, // ActionAdd 0x0A
|
||||
1, // ActionSubtract 0x0B
|
||||
1, // ActionMultiply 0x0C
|
||||
1, // ActionDivide 0x0D
|
||||
1, // ActionEqual 0x0E
|
||||
1, // ActionLessThan 0x0F
|
||||
1, // ActionLogicalAnd 0x10
|
||||
1, // ActionLogicalOr 0x11
|
||||
1, // ActionLogicalNot 0x12
|
||||
1, // ActionStringEq 0x13
|
||||
1, // ActionStringLength 0x14
|
||||
1, // ActionSubString 0x15
|
||||
0, // (empty) 0x16
|
||||
1, // ActionPop 0x17
|
||||
1, // ActionInt 0x18
|
||||
0, // 0x19
|
||||
0, // 0x1A
|
||||
0, // 0x1B
|
||||
1, // ActionGetVariable 0x1C
|
||||
1, // ActionSetVariable 0x1D
|
||||
0, // 0x1E
|
||||
0, // 0x1F
|
||||
1, // ActionSetTargetExpression 0x20
|
||||
1, // ActionStringConcat 0x21
|
||||
1, // ActionGetProperty 0x22
|
||||
1, // ActionSetProperty 0x23
|
||||
1, // ActionDuplicateClip 0x24
|
||||
1, // ActionRemoveClip 0x25
|
||||
1, // ActionTrace 0x26
|
||||
1, // ActionStartDrag 0x27
|
||||
1, // ActionStopDrag 0x28
|
||||
1, // ActionStringCompare 0x29
|
||||
1, // ActionThrow 0x2A
|
||||
1, // ActionCastOp 0x2B
|
||||
1, // ActionImplementsOp 0x2C
|
||||
0, // 0x2D
|
||||
0, // 0x2E
|
||||
0, // 0x2F
|
||||
1, // ActionRandom 0x30
|
||||
1, // ActionMBLength 0x31
|
||||
1, // ActionOrd 0x32
|
||||
1, // ActionChr 0x33
|
||||
1, // ActionGetTimer 0x34
|
||||
1, // ActionMBSubString 0x35
|
||||
1, // ActionMBOrd 0x36
|
||||
1, // ActionMBChr 0x37
|
||||
0, // 0x38
|
||||
0, // 0x39
|
||||
1, // ActionDelete 0x3A
|
||||
1, // ActionDelete2 0x3B
|
||||
1, // ActionDefineLocal 0x3C
|
||||
1, // ActionCallFunction 0x3D
|
||||
1, // ActionReturn 0x3E
|
||||
1, // ActionModulo 0x3F
|
||||
1, // ActionNew 0x40
|
||||
1, // ActionVar 0x41
|
||||
1, // ActionInitArray 0x42
|
||||
1, // ActionInitObject 0x43
|
||||
1, // ActionTypeOf 0x44
|
||||
1, // ActionTargetPath 0x45
|
||||
1, // ActionEnumerate 0x46
|
||||
1, // ActionNewAdd 0x47
|
||||
1, // ActionNewLessThan 0x48
|
||||
1, // ActionNewEquals 0x49
|
||||
1, // ActionToNumber 0x4A
|
||||
1, // ActionToString 0x4B
|
||||
1, // ActionDup 0x4C
|
||||
1, // ActionSwap 0x4D
|
||||
1, // ActionGetMember 0x4E
|
||||
1, // ActionSetMember 0x4F
|
||||
1, // ActionIncrement 0x50
|
||||
1, // ActionDecrement 0x51
|
||||
1, // ActionCallMethod 0x52
|
||||
1, // ActionNewMethod 0x53
|
||||
1, // ActionInstanceOf 0x54
|
||||
1, // ActionEnum2 0x55
|
||||
1, // EAUnknownAction56 0x56
|
||||
0, // 0x57
|
||||
1, // EAUnknownAction58 0x58
|
||||
1, // EAPushZero 0x59
|
||||
1, // EAPushOne 0x5A
|
||||
1, // EACallFunctionPop 0x5B
|
||||
1, // EACallFunction 0x5C
|
||||
1, // EACAllMethodPop 0x5D
|
||||
1, // EACallMethod 0x5E
|
||||
0, // 0x5F
|
||||
1, // ActionBitwiseAnd 0x60
|
||||
1, // ActionBitwiseOr 0x61
|
||||
1, // ActionBitwiseXor 0x62
|
||||
1, // ActionShiftLeft 0x63
|
||||
1, // ActionShiftRight 0x64
|
||||
1, // ActionShiftRight2 0x65
|
||||
1, // ActionStrictEq 0x66
|
||||
1, // ActionGreater 0x67
|
||||
1, // ActionStringGreater 0x68
|
||||
1, // ActionExtends 0x69
|
||||
0, // 0x6A
|
||||
0, // 0x6B
|
||||
0, // 0x6C
|
||||
0, // 0x6D
|
||||
0, // 0x6E
|
||||
0, // 0x6F
|
||||
1, // EAPushThis 0x70
|
||||
1, // EAPushGlobal 0x71
|
||||
1, // EAZeroVariable 0x72
|
||||
1, // EAPushTrue 0x73
|
||||
1, // EAPushFalse 0x74
|
||||
1, // EAPushNull 0x75
|
||||
1, // EAPushUndefined 0x76
|
||||
0, // 0x77
|
||||
0, // 0x78
|
||||
0, // 0x79
|
||||
0, // 0x7A
|
||||
0, // 0x7B
|
||||
0, // 0x7C
|
||||
0, // 0x7D
|
||||
0, // 0x7E
|
||||
0, // 0x7F
|
||||
0, // 0x80
|
||||
5, // ActionGotoFrame 0x81
|
||||
0, // 0x82
|
||||
9, // ActionGetURL 0x83
|
||||
0, // 0x84
|
||||
0, // 0x85
|
||||
0, // 0x86
|
||||
5, // ActionSetRegister 0x87
|
||||
9, // ActionConstantPool 0x88
|
||||
0, // 0x89
|
||||
1, // ActionWaitForFrame 0x8A
|
||||
5, // ActionSetTarget 0x8B
|
||||
5, // ActionGotoLabel 0x8C
|
||||
1, // ActionWaitForFrameExpression 0x8D
|
||||
29, // ActionDefineFunction2 0x8E
|
||||
1, // ActionTry 0x8F
|
||||
0, // 0x90
|
||||
0, // 0x91
|
||||
0, // 0x92
|
||||
0, // 0x93
|
||||
5, // ActionWith 0x94
|
||||
0, // 0x95
|
||||
9, // ActionPushData 0x96
|
||||
0, // 0x97
|
||||
0, // 0x98
|
||||
5, // ActionBranchAlways 0x99
|
||||
1, // ActionGetURL2 0x9A
|
||||
25, // ActionDefineFunction 0x9B
|
||||
0, // 0x9C
|
||||
5, // ActionBranchIfTrue 0x9D
|
||||
1, // ActionCallFrame 0x9E
|
||||
5, // ActionGotoExpression 0x9F
|
||||
0, // 0xA0
|
||||
5, // EAPushString 0xA1
|
||||
2, // EAPushConstant 0xA2
|
||||
3, // EAPushWordConstant 0xA3
|
||||
5, // EAGetStringVar 0xA4
|
||||
5, // EAGetStringMember 0xA5
|
||||
5, // EASetStringVar 0xA6
|
||||
5, // EASetStringMember 0xA7
|
||||
0, // 0xA8
|
||||
0, // 0xA9
|
||||
0, // 0xAA
|
||||
0, // 0xAB
|
||||
0, // 0xAC
|
||||
0, // 0xAD
|
||||
2, // EAPushValueOfVar 0xAE
|
||||
2, // EAGetNamedMember 0xAF
|
||||
2, // EACallNamedFunctionPop 0xB0
|
||||
2, // EACallNamedFunction 0xB1
|
||||
2, // EACallNamedMethodPop 0xB2
|
||||
2, // EACallNamedMethod 0xB3
|
||||
5, // EAPushFloat 0xB4
|
||||
2, // EAPushByte 0xB5
|
||||
3, // EAPushShort 0xB6
|
||||
5, // EAPushLong 0xB7
|
||||
5, // EABranchIfFalse 0xB8
|
||||
2 // EAPushRegister 0xB9
|
||||
];
|
||||
|
||||
// ─── Composed helpers ───
|
||||
|
||||
// Selector string for action nodes that reference constants
|
||||
const CONST_ACTION_SELECTOR = '[action=' + EA_PUSHCONSTANT + '],' +
|
||||
'[action=' + EA_CALLNAMEDFUNCTIONPOP + '],' +
|
||||
'[action=' + EA_CALLNAMEDFUNCTION + '],' +
|
||||
'[action=' + EA_CALLNAMEDMETHODPOP + '],' +
|
||||
'[action=' + EA_CALLNAMEDMETHOD + '],' +
|
||||
'[action=' + EA_PUSHVALUEOFVAR + '],' +
|
||||
'[action=' + EA_GETNAMEDMEMBER + '],' +
|
||||
'[action=' + EA_PUSHWORDCONSTANT + ']';
|
||||
|
||||
// Selector for function-splitting tag names (used in expand)
|
||||
const FUNCTION_SPLITTERS = 'definefunction2,definefunction,constantpool,end';
|
||||
|
||||
// System argument order for definefunction2
|
||||
const SYSTEM_ARGUMENTS = ['this', 'arguments', 'super', '_root', '_parent', '_global', '_extern'];
|
||||
|
||||
// ─── Exports ───
|
||||
module.exports = {
|
||||
// opcodes
|
||||
ACTION_END, ACTION_NEXTFRAME, ACTION_PREVFRAME, ACTION_PLAY, ACTION_STOP,
|
||||
ACTION_TOGGLEQUALITY, ACTION_STOPSOUNDS, ACTION_ADD, ACTION_SUBTRACT,
|
||||
ACTION_MULTIPLY, ACTION_DIVIDE, ACTION_EQUAL, ACTION_LESSTHAN,
|
||||
ACTION_LOGICALAND, ACTION_LOGICALOR, ACTION_LOGICALNOT, ACTION_STRINGEQ,
|
||||
ACTION_STRINGLENGTH, ACTION_SUBSTRING, ACTION_POP, ACTION_INT,
|
||||
ACTION_GETVARIABLE, ACTION_SETVARIABLE, ACTION_SETTARGETEXPRESSION,
|
||||
ACTION_STRINGCONCAT, ACTION_GETPROPERTY, ACTION_SETPROPERTY,
|
||||
ACTION_DUPLICATECLIP, ACTION_REMOVECLIP, ACTION_TRACE,
|
||||
ACTION_STARTDRAGMOVIE, ACTION_STOPDRAGMOVIE, ACTION_STRINGCOMPARE,
|
||||
ACTION_THROW, ACTION_CASTOP, ACTION_IMPLEMENTSOP, ACTION_RANDOM,
|
||||
ACTION_MBLENGTH, ACTION_ORD, ACTION_CHR, ACTION_GETTIMER,
|
||||
ACTION_MBSUBSTRING, ACTION_MBORD, ACTION_MBCHR, ACTION_DELETE,
|
||||
ACTION_DELETE2, ACTION_DEFINELOCAL, ACTION_CALLFUNCTION, ACTION_RETURN,
|
||||
ACTION_MODULO, ACTION_NEW, ACTION_VAR, ACTION_INITARRAY, ACTION_INITOBJECT,
|
||||
ACTION_TYPEOF, ACTION_TARGETPATH, ACTION_ENUMERATE, ACTION_NEWADD,
|
||||
ACTION_NEWLESSTHAN, ACTION_NEWEQUALS, ACTION_TONUMBER, ACTION_TOSTRING,
|
||||
ACTION_DUP, ACTION_SWAP, ACTION_GETMEMBER, ACTION_SETMEMBER,
|
||||
ACTION_INCREMENT, ACTION_DECREMENT, ACTION_CALLMETHOD, ACTION_NEWMETHOD,
|
||||
ACTION_INSTANCEOF, ACTION_ENUM2, ACTION_BITWISEAND, ACTION_BITWISEOR,
|
||||
ACTION_BITWISEXOR, ACTION_SHIFTLEFT, ACTION_SHIFTRIGHT, ACTION_SHIFTRIGHT2,
|
||||
ACTION_STRICTEQ, ACTION_GREATER, ACTION_STRINGGREATER, ACTION_EXTENDS,
|
||||
ACTION_GOTOFRAME, ACTION_GETURL, ACTION_WAITFORFRAME, ACTION_SETTARGET,
|
||||
ACTION_GOTOLABEL, ACTION_WAITFORFRAMEEXPRESSION, ACTION_PUSHDATA,
|
||||
ACTION_BRANCHALWAYS, ACTION_GETURL2, ACTION_DEFINEFUNCTION,
|
||||
ACTION_BRANCHIFTRUE, ACTION_CALLFRAME, ACTION_GOTOEXPRESSION,
|
||||
ACTION_CONSTANTPOOL, ACTION_DEFINEFUNCTION2, ACTION_TRY, ACTION_WITH,
|
||||
ACTION_SETREGISTER,
|
||||
EA_ACTION56, EA_ACTION58, EA_PUSHZERO, EA_PUSHONE,
|
||||
EA_CALLFUNCTIONPOP, EA_CALLFUNCTION, EA_CALLMETHODPOP, EA_CALLMETHOD,
|
||||
EA_PUSHTHIS, EA_PUSHGLOBAL, EA_ZEROVARIABLE, EA_PUSHTRUE, EA_PUSHFALSE,
|
||||
EA_PUSHNULL, EA_PUSHUNDEFINED, EA_ACTION77,
|
||||
EA_PUSHSTRING, EA_PUSHCONSTANT, EA_PUSHWORDCONSTANT,
|
||||
EA_GETSTRINGVAR, EA_GETSTRINGMEMBER, EA_SETSTRINGVAR, EA_SETSTRINGMEMBER,
|
||||
EA_PUSHVALUEOFVAR, EA_GETNAMEDMEMBER,
|
||||
EA_CALLNAMEDFUNCTIONPOP, EA_CALLNAMEDFUNCTION,
|
||||
EA_CALLNAMEDMETHODPOP, EA_CALLNAMEDMETHOD,
|
||||
EA_PUSHFLOAT, EA_PUSHBYTE, EA_PUSHSHORT, EA_PUSHLONG,
|
||||
EA_BRANCHIFFALSE, EA_PUSHREGISTER,
|
||||
|
||||
// flag enums
|
||||
PlaceObjectFlags, ClipEventFlags, ButtonRecordFlags,
|
||||
ButtonActionFlags, ButtonInput, FunctionPreloadFlags,
|
||||
|
||||
// name tables
|
||||
ActionNames, ActionNames2, ActionAligns, ActionSizes,
|
||||
|
||||
// composed helpers
|
||||
CONST_ACTION_SELECTOR, FUNCTION_SPLITTERS, SYSTEM_ARGUMENTS
|
||||
};
|
||||
@@ -0,0 +1,433 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
const C = require('./constants');
|
||||
const U = require('./utils');
|
||||
|
||||
// ─── Main expand function ───
|
||||
function expand(input, output, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const { filedir, aptName, baseName } = U.resolveFilePaths(input);
|
||||
const tmpdir = U.createTempDir();
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
// 1. Copy files to temp
|
||||
const { manifestData, datData } = U.copyInputToTemp(filedir, tmpdir, baseName, aptName);
|
||||
|
||||
// 2. Run apt2xml.exe
|
||||
U.runApt2Xml(opts.toolsDir, tmpdir, baseName);
|
||||
|
||||
// 3. Read raw XML
|
||||
const rawXml = U.readRawXml(tmpdir, baseName);
|
||||
|
||||
// 4. Parse manifest if available (for texture name resolution)
|
||||
const manifest = manifestData ? cheerio.load(manifestData, { xmlMode: true, decodeEntities: false }) : null;
|
||||
|
||||
// 5. Parse .dat if available
|
||||
const dats = parseDat(datData);
|
||||
|
||||
// 6. Run xmladdhint() transformation
|
||||
const humanXml = xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, opts.oldStyle);
|
||||
|
||||
// 7. Write output
|
||||
const outPath = output || (filedir + baseName + '_edit.xml');
|
||||
fs.writeFileSync(outPath, humanXml, 'utf-8');
|
||||
|
||||
success = true;
|
||||
return outPath;
|
||||
} finally {
|
||||
if (!opts.keepTemp) {
|
||||
U.cleanupTempDir(tmpdir, false);
|
||||
} else {
|
||||
console.warn('Temporary files kept in: ' + tmpdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Parse .dat file ───
|
||||
function parseDat(datData) {
|
||||
const dats = {};
|
||||
if (!datData) return dats;
|
||||
const lines = datData.split(/[\r\n]+/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let m;
|
||||
if ((m = lines[i].match(/([0-9]+)\-\>([0-9]+)/))) {
|
||||
dats[m[1]] = m[2];
|
||||
} else if ((m = lines[i].match(/([0-9]+)\=([0-9\s]+)/))) {
|
||||
dats[m[1]] = m[2].split(/\s+/);
|
||||
}
|
||||
}
|
||||
return dats;
|
||||
}
|
||||
|
||||
// ─── xmladdhint(): raw apt XML → human-readable XML ───
|
||||
function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle) {
|
||||
U.resetLabels();
|
||||
|
||||
const $ = cheerio.load(rawXml, { xmlMode: true, decodeEntities: false });
|
||||
const ActionNames = oldStyle ? C.ActionNames2 : C.ActionNames;
|
||||
const xmldoc = $;
|
||||
|
||||
// Remove duplicated exports
|
||||
const exp = {};
|
||||
$('export').each(function () {
|
||||
const nm = $(this).attr('name');
|
||||
if (exp[nm]) {
|
||||
$(this).remove();
|
||||
} else {
|
||||
exp[nm] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate aligned positions for each action container
|
||||
const actionContainers = $('initaction, buttonaction, action, clipaction');
|
||||
actionContainers.each(function () {
|
||||
const list = $(this).children('[action]');
|
||||
U.calcaln(list);
|
||||
});
|
||||
|
||||
// Add end-of-function markers
|
||||
const functions = $(C.FUNCTION_SPLITTERS);
|
||||
functions.each(function () {
|
||||
const v = $(this);
|
||||
if (v[0].tagName.indexOf('definefunction') === 0) {
|
||||
const size = parseInt(v.attr('size'), 10);
|
||||
const headaln = U.thisaln(v.next());
|
||||
let c = null;
|
||||
let sz = 0;
|
||||
let cur = v;
|
||||
for (; sz <= size && cur.length; ) {
|
||||
c = cur;
|
||||
sz = U.nextaln(c) - headaln;
|
||||
if (sz === size) {
|
||||
break;
|
||||
}
|
||||
cur = cur.next();
|
||||
c = null;
|
||||
}
|
||||
if (c) {
|
||||
const endNode = U.el('end', $);
|
||||
endNode.insertAfter(c);
|
||||
endNode.attr('aln', U.nextaln(c));
|
||||
U.copyindent(endNode, c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve constants (expand constant pool references)
|
||||
const actionConsts = $(C.CONST_ACTION_SELECTOR);
|
||||
actionConsts.each(function () {
|
||||
const v = $(this);
|
||||
const val = parseInt(v.attr('val'), 10);
|
||||
const c = v.parent().find('constantpool').children().eq(val);
|
||||
if (c.attr('str') !== undefined) {
|
||||
v.attr('str', c.attr('str'));
|
||||
} else {
|
||||
v.attr('num', c.attr('num'));
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve jump targets
|
||||
const gotos = $('goto');
|
||||
gotos.each(function () {
|
||||
const v = $(this);
|
||||
const action = parseInt(v.attr('action'), 10);
|
||||
if (action === C.ACTION_BRANCHALWAYS || action === C.EA_BRANCHIFFALSE || action === C.ACTION_BRANCHIFTRUE) {
|
||||
let pos = parseInt(v.attr('pos'), 10);
|
||||
let targetlabel = v.attr('targetlabel');
|
||||
if (!targetlabel) {
|
||||
const naln = U.nextaln(v);
|
||||
const jumpaln = naln + pos;
|
||||
const filter = '[aln=' + jumpaln + ']:first';
|
||||
const target = pos >= 0 ? v.nextAll(filter) : v.prevAll(filter);
|
||||
if (target.length) {
|
||||
let label = target.attr('mylabel');
|
||||
if (!label) {
|
||||
label = U.makelabel();
|
||||
target.attr('mylabel', label);
|
||||
}
|
||||
v.attr('targetlabel', label);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert actions to text
|
||||
actionContainers.each(function () {
|
||||
const v = $(this);
|
||||
let astr = '';
|
||||
const actions = v.children().not('constantpool');
|
||||
const indent = [U.indentstr(actions.first())];
|
||||
actions.each(function () {
|
||||
const a = $(this);
|
||||
if (a.attr('mylabel')) {
|
||||
astr += indent[0];
|
||||
astr += a.attr('mylabel') + ':';
|
||||
}
|
||||
if (a[0].tagName === 'end') {
|
||||
indent.pop();
|
||||
}
|
||||
astr += indent.join('');
|
||||
astr += actiontostring(a, ActionNames, $);
|
||||
if (a[0].tagName.indexOf('definefunction') === 0) {
|
||||
indent.push('\t');
|
||||
}
|
||||
});
|
||||
astr += U.indentstr(v);
|
||||
// Replace CDATA with text node (entity-encoded)
|
||||
v.empty().text(astr);
|
||||
});
|
||||
|
||||
// Resolve flags to symbolic names
|
||||
$('placeobject').each(function () {
|
||||
$(this).attr('flags', U.valuetoflags($(this).attr('flags'), C.PlaceObjectFlags));
|
||||
});
|
||||
$('buttonrecord').each(function () {
|
||||
$(this).attr('flags', U.valuetoflags($(this).attr('flags'), C.ButtonRecordFlags));
|
||||
});
|
||||
$('clipaction').each(function () {
|
||||
const $v = $(this);
|
||||
const flags = parseInt($v.attr('flags'), 10);
|
||||
const realflags = U.endian(flags & 0xffffff, 3);
|
||||
const keycode = (flags >> 24) & 0xff;
|
||||
$v.attr('flags', U.valuetoflags(realflags, C.ClipEventFlags));
|
||||
$v.attr('keycode', keycode);
|
||||
});
|
||||
$('buttonaction').each(function () {
|
||||
const $v = $(this);
|
||||
const flags = parseInt($v.attr('flags'), 10);
|
||||
$v.attr('flags', U.valuetoflags(flags, C.ButtonActionFlags));
|
||||
});
|
||||
|
||||
// Image → texture name resolution (from manifest)
|
||||
if (manifest) {
|
||||
$('image').each(function () {
|
||||
const v = $(this);
|
||||
const id = v.attr('id');
|
||||
const dd = dats[id];
|
||||
if (typeof dd === 'string') {
|
||||
const tex = manifest(`Texture[id=apt_${baseName}_${dd}]`).attr('File');
|
||||
if (tex) v.attr('image', tex);
|
||||
} else if (dd) {
|
||||
const tex = manifest(`Texture[id=apt_${baseName}_${id}]`).attr('File');
|
||||
if (tex) v.attr('image', tex + ',' + dd.join(' '));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Shape → embed .ru geometry
|
||||
$('shape').each(function () {
|
||||
const $v = $(this);
|
||||
const id = $v.attr('id');
|
||||
let ff = null;
|
||||
if (manifest) {
|
||||
ff = manifest(`AptGeometryData[id=${baseName}_${id}]`).attr('File');
|
||||
} else {
|
||||
ff = $v.attr('geometry');
|
||||
}
|
||||
if (ff) {
|
||||
let ruData = '';
|
||||
try {
|
||||
ruData = fs.readFileSync(filedir + ff, 'utf-8');
|
||||
} catch (e) {
|
||||
// ru file might not exist
|
||||
}
|
||||
if (ruData) {
|
||||
let ru = '\n' + ruData;
|
||||
ru = ru.split(/[\r\n]+/);
|
||||
for (let i = 0; i < ru.length; i++) {
|
||||
ru[i] = ru[i].trim();
|
||||
if (ru[i].indexOf('tc') > 0) {
|
||||
const ts = ru[i].split(':');
|
||||
const img = $('image[id=' + ts[5] + ']');
|
||||
ts[5] = img.length ? (img.attr('image') || ts[5]) : ts[5];
|
||||
ru[i] = ts.join(':');
|
||||
}
|
||||
}
|
||||
ru = ru.join('\n\t\t\t') + '\n\t\t';
|
||||
$v.attr('geometry', ff);
|
||||
const ruNode = U.el('ru', $);
|
||||
ruNode.text(ru);
|
||||
$v.append('\n\t\t');
|
||||
$v.append(ruNode);
|
||||
$v.append('\n\t');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up placeobject attributes based on flags
|
||||
$('placeobject').each(function () {
|
||||
const $v = $(this);
|
||||
const flags = $v.attr('flags') || '';
|
||||
if (flags.indexOf('HasCharacter') < 0 || $v.attr('character') === '-1') {
|
||||
$v.removeAttr('character');
|
||||
}
|
||||
if (flags.indexOf('HasMatrix') < 0) {
|
||||
$v.removeAttr('matrix');
|
||||
}
|
||||
if (flags.indexOf('HasColorTransform') < 0) {
|
||||
$v.removeAttr('rgbamul rgbaadd');
|
||||
}
|
||||
if (flags.indexOf('HasRatio') < 0) {
|
||||
$v.removeAttr('ratio');
|
||||
}
|
||||
if (flags.indexOf('HasClipDepth') < 0) {
|
||||
$v.removeAttr('clipdepth');
|
||||
}
|
||||
if (flags.indexOf('HasName') < 0) {
|
||||
$v.find('poname').remove();
|
||||
}
|
||||
if (flags.indexOf('HasClipAction') < 0) {
|
||||
$v.find('clipactions').remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up: remove extra attributes from the final output
|
||||
$('edittext').each(function () {
|
||||
const $v = $(this);
|
||||
// Override for xmladdhint: store original color info
|
||||
// (the original code swaps red/blue for edittext display - preserve that)
|
||||
});
|
||||
|
||||
// Clean internal attributes
|
||||
$('*').each(function () {
|
||||
const $v = $(this);
|
||||
$v.removeAttr('filtered mylabel targetlabel aln');
|
||||
});
|
||||
|
||||
// Serialize
|
||||
let output = $.xml();
|
||||
// Wrap action container text content in CDATA sections
|
||||
// This prevents cheerio from entity-encoding & in the action assembly text
|
||||
output = output.replace(
|
||||
/<(initaction|buttonaction|action|clipaction)(\s[^>]*)?>([\s\S]*?)<\/\1>/g,
|
||||
(match, tag, attrs, content) => {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) return match;
|
||||
return '<' + tag + (attrs || '') + '><![CDATA[' + content + ']]></' + tag + '>';
|
||||
}
|
||||
);
|
||||
// Clean up excessive whitespace
|
||||
output = output.replace(/^[\t]+$/gm, '').replace(/[\r\n]+/g, '\n');
|
||||
return output;
|
||||
}
|
||||
|
||||
// ─── Decode XML entities in attribute values ───
|
||||
// With decodeEntities: false in cheerio load, attribute values preserve XML entities
|
||||
// (e.g. & stays as &). We need to decode them for human-readable output.
|
||||
function decodeAttrEntities(s) {
|
||||
if (typeof s !== 'string') return s;
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// ─── actiontostring(): XML action node → text assembly ───
|
||||
function actiontostring(a, ActionNames, $) {
|
||||
const action = parseInt(a.attr('action'), 10);
|
||||
let aname = ActionNames[action] || ('unknown_' + action);
|
||||
|
||||
// Fix sign extension for pushbyte/pushshort
|
||||
if (action === C.EA_PUSHBYTE) {
|
||||
a.attr('val', (a.attr('val') << 24) >> 24);
|
||||
} else if (action === C.EA_PUSHSHORT) {
|
||||
a.attr('val', (a.attr('val') << 16) >> 16);
|
||||
}
|
||||
|
||||
// Branch instructions: jump label_name
|
||||
if (action === C.ACTION_BRANCHALWAYS || action === C.ACTION_BRANCHIFTRUE || action === C.EA_BRANCHIFFALSE) {
|
||||
return aname + ' ' + a.attr('targetlabel');
|
||||
}
|
||||
|
||||
// Constant-referencing instructions: pushconst "str" or pushconst 123
|
||||
if (action === C.EA_PUSHCONSTANT || action === C.EA_CALLNAMEDFUNCTIONPOP ||
|
||||
action === C.EA_CALLNAMEDFUNCTION || action === C.EA_CALLNAMEDMETHODPOP ||
|
||||
action === C.EA_CALLNAMEDMETHOD || action === C.EA_PUSHVALUEOFVAR ||
|
||||
action === C.EA_GETNAMEDMEMBER || action === C.EA_PUSHWORDCONSTANT) {
|
||||
return aname + ' ' + (a.attr('str') !== undefined ? U.mkstr(decodeAttrEntities(a.attr('str'))) : a.attr('num'));
|
||||
}
|
||||
|
||||
// goto: ActionGotoFrame pos
|
||||
if (a[0].tagName === 'goto') {
|
||||
return aname + ' ' + a.attr('pos');
|
||||
}
|
||||
|
||||
// geturl
|
||||
if (a[0].tagName === 'geturl') {
|
||||
return aname + ' ' + U.mkstr(decodeAttrEntities(a.attr('str1'))) + ', ' + U.mkstr(decodeAttrEntities(a.attr('str2')));
|
||||
}
|
||||
|
||||
// definefunction
|
||||
if (a[0].tagName === 'definefunction') {
|
||||
let s1 = 'function ' + a.attr('name') + '(';
|
||||
const args = [];
|
||||
a.children().each(function () {
|
||||
args.push($(this).attr('name'));
|
||||
});
|
||||
s1 += args.join(', ') + ')';
|
||||
return s1;
|
||||
}
|
||||
|
||||
// definefunction2
|
||||
if (a[0].tagName === 'definefunction2') {
|
||||
const flags = parseInt(a.attr('flags'), 10);
|
||||
const nregs = flags & 0xff;
|
||||
const realflags = U.endian((flags >> 8) & 0xffffff, 3);
|
||||
let s1 = 'function ' + a.attr('name') + '(';
|
||||
const aa = [];
|
||||
const aa2 = ['regs:' + nregs];
|
||||
let r = 1;
|
||||
const fp = C.FunctionPreloadFlags;
|
||||
if (realflags & fp.PreloadThis) aa2.push('reg' + (r++) + ':this');
|
||||
if (realflags & fp.PreloadArguments) aa2.push('reg' + (r++) + ':arguments');
|
||||
if (realflags & fp.PreloadSuper) aa2.push('reg' + (r++) + ':super');
|
||||
if (realflags & fp.PreloadRoot) aa2.push('reg' + (r++) + ':_root');
|
||||
if (realflags & fp.PreloadParent) aa2.push('reg' + (r++) + ':_parent');
|
||||
if (realflags & fp.PreloadGlobal) aa2.push('reg' + (r++) + ':_global');
|
||||
if (realflags & fp.PreloadExtern) aa2.push('reg' + (r++) + ':_extern');
|
||||
|
||||
a.children().each(function () {
|
||||
aa.push('reg' + $(this).attr('reg') + ':' + $(this).attr('name'));
|
||||
});
|
||||
s1 += aa.join(', ') + ')(' + aa2.join(', ') + ')';
|
||||
return s1;
|
||||
}
|
||||
|
||||
// end
|
||||
if (a[0].tagName === 'end') {
|
||||
return 'end';
|
||||
}
|
||||
|
||||
// noarg (single opcode with no arguments)
|
||||
if (a[0].tagName === 'noarg') {
|
||||
return aname;
|
||||
}
|
||||
|
||||
// string instruction: pushstr "hello"
|
||||
if (a[0].tagName === 'string') {
|
||||
return aname + ' ' + U.mkstr(decodeAttrEntities(a.attr('str')));
|
||||
}
|
||||
|
||||
// pushdata: pushdata val1, val2, ...
|
||||
if (a[0].tagName === 'pushdata') {
|
||||
const vals = [];
|
||||
a.children().each(function () {
|
||||
const v = $(this);
|
||||
vals.push(v.attr('str') !== undefined ? U.mkstr(decodeAttrEntities(v.attr('str'))) : v.attr('num'));
|
||||
});
|
||||
return aname + ' ' + vals.join(', ');
|
||||
}
|
||||
|
||||
// Generic: actionname value
|
||||
if (a.attr('val') !== undefined) {
|
||||
return aname + ' ' + a.attr('val');
|
||||
}
|
||||
|
||||
return 'unknownaction_' + action;
|
||||
}
|
||||
|
||||
// ─── Exports ───
|
||||
module.exports = { expand };
|
||||
@@ -0,0 +1,266 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const cp = require('child_process');
|
||||
|
||||
const {
|
||||
ActionSizes, ActionAligns, PlaceObjectFlags, ClipEventFlags,
|
||||
ButtonRecordFlags, ButtonActionFlags, FunctionPreloadFlags
|
||||
} = require('./constants');
|
||||
|
||||
// ─── Byte order ───
|
||||
function endian(n, bytes) {
|
||||
bytes = bytes || 4;
|
||||
let v = 0;
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
const b = n & 0xff;
|
||||
v |= b << (8 * (bytes - i - 1));
|
||||
n >>>= 8;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ─── Flag converters ───
|
||||
function valuetoflags(v, e) {
|
||||
v = parseInt(v, 10);
|
||||
let v0 = v;
|
||||
const keys = [];
|
||||
for (const k in e) {
|
||||
const ek = e[k];
|
||||
if (v & ek) {
|
||||
keys.push(k);
|
||||
v0 &= ~ek;
|
||||
}
|
||||
}
|
||||
if (v0) {
|
||||
throw new Error('unknown bit flags ' + v0 + ' in ' + v);
|
||||
}
|
||||
return keys.join('|');
|
||||
}
|
||||
|
||||
function flagstovalue(f, e) {
|
||||
if (!f || f === '') return 0;
|
||||
const parts = f.split('|');
|
||||
let v = 0;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const k = e[parts[i]];
|
||||
if (k !== undefined) {
|
||||
v |= k;
|
||||
} else {
|
||||
throw new Error('invalid bit flag: ' + parts[i]);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ─── String encoding ───
|
||||
function encodeXML(s) {
|
||||
const a = new Array(s.length);
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s[i];
|
||||
if (c === '\r') a[i] = '\\r';
|
||||
else if (c === '\n') a[i] = '\\n';
|
||||
else if (c === '"') a[i] = '\\"';
|
||||
else a[i] = c;
|
||||
}
|
||||
return a.join('');
|
||||
}
|
||||
|
||||
function mkstr(s) {
|
||||
return '"' + encodeXML(s) + '"';
|
||||
}
|
||||
|
||||
// ─── Cheerio element creation (works with a cheerio $ instance) ───
|
||||
function el(tagName, $) {
|
||||
return $('<' + tagName + '>');
|
||||
}
|
||||
|
||||
function bad(t, $) {
|
||||
return $('<bad>').attr('text', encodeURIComponent(t));
|
||||
}
|
||||
|
||||
// ─── Label generation ───
|
||||
let labelID = 0;
|
||||
function makelabel() {
|
||||
return 'lxnx' + labelID++;
|
||||
}
|
||||
function resetLabels() {
|
||||
labelID = 0;
|
||||
}
|
||||
|
||||
// ─── Indentation helpers ───
|
||||
function indentstr(n) {
|
||||
const t = n[0].prev;
|
||||
if (t && t.nodeType === 3) {
|
||||
return t.data;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function copyindent(ndest, nsrc) {
|
||||
const indent = nsrc[0].prev;
|
||||
if (indent && indent.nodeType === 3) {
|
||||
const indent2 = ndest[0].prev;
|
||||
if (indent2 && indent2.nodeType === 3) {
|
||||
$(indent2).remove();
|
||||
}
|
||||
ndest.before(indent.cloneNode());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RGBA helpers ───
|
||||
function rgba(c) {
|
||||
c = parseInt(c, 10);
|
||||
return [
|
||||
c & 0xff,
|
||||
(c >> 8) & 0xff,
|
||||
(c >> 16) & 0xff,
|
||||
(c >> 24) & 0xff
|
||||
].join(',');
|
||||
}
|
||||
|
||||
function getrgba($v) {
|
||||
return $v.attr('red') + ',' + $v.attr('green') + ',' + $v.attr('blue') + ',' + $v.attr('alpha');
|
||||
}
|
||||
|
||||
function cleanrgba($v) {
|
||||
$v.removeAttr('red green blue alpha');
|
||||
}
|
||||
|
||||
function getmatrix($v) {
|
||||
return [
|
||||
$v.attr('rotm00'), $v.attr('rotm01'),
|
||||
$v.attr('rotm10'), $v.attr('rotm11'),
|
||||
$v.attr('tx'), $v.attr('ty')
|
||||
].join(',');
|
||||
}
|
||||
|
||||
function cleanmatrix($v) {
|
||||
$v.removeAttr('rotm00 rotm01 rotm10 rotm11 tx ty');
|
||||
}
|
||||
|
||||
// ─── Action alignment helpers ───
|
||||
function alnsize(l, aln) {
|
||||
if (l.is('end')) return 0;
|
||||
const la = parseInt(l.attr('action'), 10);
|
||||
const aa = ActionAligns[la];
|
||||
let sz = ActionSizes[la];
|
||||
if (aa && ((aln + 1) % 4)) {
|
||||
sz += 4 - ((aln + 1) % 4);
|
||||
}
|
||||
return sz;
|
||||
}
|
||||
|
||||
function calcaln(list) {
|
||||
let aln = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const l = list.eq(i);
|
||||
const sz = alnsize(l, aln);
|
||||
l.attr('aln', aln);
|
||||
aln += sz;
|
||||
}
|
||||
}
|
||||
|
||||
function thisaln(l) {
|
||||
return parseInt(l.attr('aln'), 10);
|
||||
}
|
||||
|
||||
function nextaln(l) {
|
||||
const aln = thisaln(l);
|
||||
return aln + alnsize(l, aln);
|
||||
}
|
||||
|
||||
// ─── Temporary directory management ───
|
||||
function createTempDir() {
|
||||
return fs.mkdtempSync(os.tmpdir() + path.sep) + path.sep;
|
||||
}
|
||||
|
||||
function cleanupTempDir(tmpdir, keepTemp) {
|
||||
if (!keepTemp && tmpdir) {
|
||||
try {
|
||||
fs.rmSync(tmpdir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Run cc3tools converter ───
|
||||
function runApt2Xml(toolsDir, tmpdir, baseName) {
|
||||
cp.execFileSync(toolsDir + 'apt2xml', [tmpdir + baseName]);
|
||||
}
|
||||
|
||||
function runXml2Apt(toolsDir, tmpdir, baseName) {
|
||||
cp.execFileSync(toolsDir + 'xml2apt', [tmpdir + baseName]);
|
||||
}
|
||||
|
||||
// ─── Resolve file paths ───
|
||||
function resolveFilePaths(aptFilePath) {
|
||||
const aptFullPath = path.resolve(aptFilePath);
|
||||
const filedir = aptFullPath.substring(0, aptFullPath.lastIndexOf(path.sep)) + path.sep;
|
||||
const aptName = aptFullPath.substring(aptFullPath.lastIndexOf(path.sep) + 1);
|
||||
const baseName = aptName.replace(/\.apt$/i, '');
|
||||
return { filedir, aptName, baseName };
|
||||
}
|
||||
|
||||
// ─── Copy input files to temp dir ───
|
||||
function copyInputToTemp(filedir, tmpdir, baseName, aptName) {
|
||||
// Always copy .apt and .const
|
||||
fs.copyFileSync(filedir + aptName, tmpdir + aptName);
|
||||
try {
|
||||
fs.copyFileSync(filedir + baseName + '.const', tmpdir + baseName + '.const');
|
||||
} catch (e) {
|
||||
// .const file might not exist
|
||||
}
|
||||
// Also copy .xml (manifest) if it exists
|
||||
let manifestData = null;
|
||||
try {
|
||||
manifestData = fs.readFileSync(filedir + baseName + '.xml', 'utf-8');
|
||||
} catch (e) {
|
||||
// manifest doesn't exist, that's ok
|
||||
}
|
||||
// Also copy .dat if it exists
|
||||
let datData = null;
|
||||
try {
|
||||
datData = fs.readFileSync(filedir + baseName + '.dat', 'utf-8');
|
||||
} catch (e) {
|
||||
// .dat doesn't exist
|
||||
}
|
||||
return { manifestData, datData };
|
||||
}
|
||||
|
||||
// ─── Copy output files from temp to filedir ───
|
||||
function copyOutputFromTemp(filedir, tmpdir, baseName, aptName) {
|
||||
// Copy .apt and .const back
|
||||
fs.copyFileSync(tmpdir + aptName, filedir + aptName);
|
||||
try {
|
||||
fs.copyFileSync(tmpdir + baseName + '.const', filedir + baseName + '.const');
|
||||
} catch (e) {
|
||||
// .const may not have been generated
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Read raw XML from temp dir ───
|
||||
function readRawXml(tmpdir, baseName) {
|
||||
return fs.readFileSync(tmpdir + baseName + '.xml', 'utf-8');
|
||||
}
|
||||
|
||||
function writeRawXml(tmpdir, baseName, xmlStr) {
|
||||
fs.writeFileSync(tmpdir + baseName + '.xml', xmlStr, 'utf-8');
|
||||
}
|
||||
|
||||
// ─── Exports ───
|
||||
module.exports = {
|
||||
endian, valuetoflags, flagstovalue,
|
||||
encodeXML, mkstr,
|
||||
el, bad, makelabel, resetLabels,
|
||||
indentstr, copyindent,
|
||||
rgba, getrgba, cleanrgba, getmatrix, cleanmatrix,
|
||||
alnsize, calcaln, thisaln, nextaln,
|
||||
createTempDir, cleanupTempDir,
|
||||
runApt2Xml, runXml2Apt,
|
||||
resolveFilePaths, copyInputToTemp, copyOutputFromTemp,
|
||||
readRawXml, writeRawXml
|
||||
};
|
||||
Generated
+1832
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "ra3apt-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "RA3 .apt file editor CLI — convert between binary .apt and human-readable XML",
|
||||
"main": "./bin/ra3apt.js",
|
||||
"bin": {
|
||||
"ra3apt": "./bin/ra3apt.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"commander": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yao-pkg/pkg": "^6.12.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user