267 lines
6.3 KiB
JavaScript
267 lines
6.3 KiB
JavaScript
'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
|
|
};
|