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.
272 lines
6.5 KiB
JavaScript
272 lines
6.5 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) {
|
|
// Keep unlabelled converter bits explicit, without assigning guessed meanings.
|
|
keys.push('0x' + (v0 >>> 0).toString(16));
|
|
}
|
|
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 if (/^0x[0-9a-f]{1,8}$/i.test(parts[i])) {
|
|
v |= Number(parts[i]);
|
|
} 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 === '\\') a[i] = '\\\\';
|
|
else if (c === '\r') a[i] = '\\r';
|
|
else if (c === '\n') a[i] = '\\n';
|
|
else if (c === '\t') a[i] = '\\t';
|
|
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
|
|
};
|