1009 lines
33 KiB
JavaScript
1009 lines
33 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const cheerio = require('cheerio');
|
|
|
|
const C = require('./constants');
|
|
const U = require('./utils');
|
|
|
|
// ─── Main build function ───
|
|
function build(input, output, opts) {
|
|
opts = opts || {};
|
|
|
|
// Read the human-readable XML
|
|
const humanXml = fs.readFileSync(input, 'utf-8');
|
|
const inputPath = path.resolve(input);
|
|
const inputDir = inputPath.substring(0, inputPath.lastIndexOf(path.sep)) + path.sep;
|
|
const inputName = inputPath.substring(inputPath.lastIndexOf(path.sep) + 1);
|
|
// Derive a default base name from the input file
|
|
let defaultBase = inputName;
|
|
if (defaultBase.endsWith('_edit.xml')) {
|
|
defaultBase = defaultBase.slice(0, -9);
|
|
} else if (defaultBase.endsWith('.xml')) {
|
|
defaultBase = defaultBase.slice(0, -4);
|
|
}
|
|
const defaultAptName = defaultBase + '.apt';
|
|
const outputPath = output || (inputDir + defaultAptName);
|
|
|
|
// Determine output directory and APT file name
|
|
const outDir = path.resolve(outputPath).substring(0, path.resolve(outputPath).lastIndexOf(path.sep)) + path.sep;
|
|
const outputAptName = path.basename(outputPath);
|
|
// Base name always comes from the output .apt file name
|
|
let baseName = outputAptName;
|
|
if (baseName.endsWith('.apt')) {
|
|
baseName = baseName.slice(0, -4);
|
|
} else if (baseName.endsWith('.const')) {
|
|
baseName = baseName.slice(0, -6);
|
|
}
|
|
const aptName = baseName + '.apt';
|
|
|
|
const tmpdir = U.createTempDir();
|
|
let success = false;
|
|
|
|
try {
|
|
// Run xmlcleanhint() transformation
|
|
const result = xmlcleanhint(humanXml, baseName, outDir, tmpdir, opts.oldStyle);
|
|
|
|
// Write raw XML to temp dir
|
|
U.writeRawXml(tmpdir, baseName, result.rawXml);
|
|
|
|
// Run xml2apt.exe
|
|
U.runXml2Apt(opts.toolsDir, tmpdir, baseName);
|
|
|
|
// Ensure output directory exists
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
|
|
// Copy .apt and .const to output directory
|
|
U.copyOutputFromTemp(outDir, tmpdir, baseName, aptName);
|
|
|
|
// Write manifest, .dat, .ru files
|
|
if (result.manifest) {
|
|
fs.writeFileSync(outDir + baseName + '.xml', result.manifest, 'utf-8');
|
|
}
|
|
if (result.dat) {
|
|
fs.writeFileSync(outDir + baseName + '.dat', result.dat, 'utf-8');
|
|
}
|
|
if (result.ruCache) {
|
|
for (const [rup, data] of Object.entries(result.ruCache)) {
|
|
// Ensure subdirectory exists for .ru files (e.g. geometry/2.ru)
|
|
fs.mkdirSync(path.dirname(rup), { recursive: true });
|
|
fs.writeFileSync(rup, data, 'utf-8');
|
|
}
|
|
}
|
|
|
|
success = true;
|
|
return outputPath;
|
|
} finally {
|
|
if (!opts.keepTemp) {
|
|
U.cleanupTempDir(tmpdir, false);
|
|
} else {
|
|
console.warn('Temporary files kept in: ' + tmpdir);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── xmlcleanhint(): human-readable XML → raw apt XML ───
|
|
function xmlcleanhint(humanXml, baseName, outDir, tmpdir, oldStyle) {
|
|
U.resetLabels();
|
|
|
|
const $ = cheerio.load(humanXml, { xmlMode: true, decodeEntities: false });
|
|
const ActionNames = oldStyle ? C.ActionNames2 : C.ActionNames;
|
|
let isbad = false;
|
|
|
|
// ──────────────────────────────────────────
|
|
// 1. Parse text assembly back to action nodes
|
|
// ──────────────────────────────────────────
|
|
const actionContainers = $('initaction, buttonaction, action, clipaction');
|
|
let lastlabel = '';
|
|
actionContainers.each(function (j) {
|
|
if (isbad) return;
|
|
const v = $(this);
|
|
let text = v.text();
|
|
text = text.split(/[\r\n]+/);
|
|
v.empty();
|
|
const aa = [];
|
|
const fstk = [];
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
const action = stringtoaction(text[i], ActionNames, $);
|
|
if (typeof action === 'string') {
|
|
if (action[0] !== ';') lastlabel = action;
|
|
} else if (action) {
|
|
if (action.is('bad')) {
|
|
isbad = true;
|
|
throw new Error('Incorrect script line: ' + text[i].trim() + '\n' +
|
|
'In container: ' + v[0].tagName);
|
|
}
|
|
if (lastlabel) {
|
|
action.attr('mylabel', lastlabel);
|
|
lastlabel = '';
|
|
}
|
|
if (action[0].tagName.indexOf('function') > 0) {
|
|
fstk.push([text[i], i]);
|
|
} else if (action[0].tagName === 'end') {
|
|
if (fstk.length === 0) {
|
|
throw new Error('Unexpected end of function');
|
|
}
|
|
fstk.pop();
|
|
}
|
|
aa.push(action);
|
|
}
|
|
}
|
|
if (fstk.length > 0) {
|
|
throw new Error('No end of function for: ' + fstk[0][0]);
|
|
}
|
|
v.append(aa);
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 2. Rebuild constant pools
|
|
// ──────────────────────────────────────────
|
|
const sortf = function (a, b) {
|
|
return a.tagName > b.tagName ? 1 : -1;
|
|
};
|
|
|
|
const actionConstants = $('data,' + C.CONST_ACTION_SELECTOR);
|
|
// Sort the collection
|
|
Array.prototype.sort.call(actionConstants, sortf);
|
|
|
|
const constmap = { str: {}, num: {} };
|
|
actionConstants.each(function () {
|
|
if ($(this).is('[str]')) {
|
|
constmap.str[$(this).attr('str')] = 1;
|
|
} else if ($(this).is('[num]')) {
|
|
constmap.num[$(this).attr('num')] = 1;
|
|
}
|
|
});
|
|
|
|
let idx = 0;
|
|
const consttable = [];
|
|
for (const k in constmap.str) {
|
|
constmap.str[k] = idx;
|
|
consttable[idx++] = k;
|
|
}
|
|
for (const k in constmap.num) {
|
|
constmap.num[k] = idx;
|
|
consttable[idx++] = parseInt(k, 10);
|
|
}
|
|
|
|
// Replace constant references with pool indices
|
|
const dataConstants = $('data');
|
|
dataConstants.each(function () {
|
|
const $v = $(this);
|
|
if ($v.is('[str]')) {
|
|
$v.attr('val', constmap.str[$v.attr('str')]);
|
|
} else if ($v.is('[num]')) {
|
|
$v.attr('val', constmap.num[$v.attr('num')]);
|
|
}
|
|
});
|
|
|
|
let err = false;
|
|
actionContainers.each(function () {
|
|
const localstr = {};
|
|
const localnum = {};
|
|
const localconst = [];
|
|
let ci = 0;
|
|
const myactions = $(this).find(C.CONST_ACTION_SELECTOR);
|
|
Array.prototype.sort.call(myactions, sortf);
|
|
|
|
myactions.each(function () {
|
|
const a = $(this);
|
|
let li, k;
|
|
if (a.is('[str]')) {
|
|
k = a.attr('str');
|
|
if (localstr[k] === undefined) {
|
|
localstr[k] = ci;
|
|
localconst[ci++] = k;
|
|
}
|
|
li = localstr[k];
|
|
a.attr('val', li);
|
|
} else if (a.is('[num]')) {
|
|
k = a.attr('num');
|
|
if (localnum[k] === undefined) {
|
|
localnum[k] = ci;
|
|
localconst[ci++] = parseInt(k, 10);
|
|
}
|
|
li = localnum[k];
|
|
a.attr('val', li);
|
|
}
|
|
if (li > 255 && !a.is('short')) {
|
|
throw new Error('Byte value exceeds 255');
|
|
}
|
|
});
|
|
|
|
if (localconst.length) {
|
|
const ct = U.el('constantpool', $);
|
|
ct.attr('action', C.ACTION_CONSTANTPOOL);
|
|
for (let x = 0; x < localconst.length; x++) {
|
|
const cc = U.el('constant', $);
|
|
if (typeof localconst[x] === 'string') {
|
|
cc.attr('val', constmap.str[localconst[x]]);
|
|
cc.attr('str', localconst[x]);
|
|
} else {
|
|
cc.attr('val', constmap.num[localconst[x]]);
|
|
cc.attr('num', localconst[x]);
|
|
}
|
|
ct.append(cc);
|
|
}
|
|
$(this).prepend(ct);
|
|
}
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 3. Write .const binary file to tmpdir
|
|
// ──────────────────────────────────────────
|
|
const cf = fs.openSync(tmpdir + baseName + '.const', 'w');
|
|
const constbuf = Buffer.alloc(0x20);
|
|
constbuf.asciiWrite('Apt constant file\x1A\0\0');
|
|
constbuf.writeUInt32LE(111, 0x14);
|
|
constbuf.writeUInt32LE(consttable.length, 0x18);
|
|
constbuf.writeUInt32LE(0x20, 0x1c);
|
|
fs.writeSync(cf, constbuf, 0, 0x20);
|
|
|
|
const tmpbuf = Buffer.alloc(8);
|
|
const strbuf = Buffer.alloc(512);
|
|
let strpos = 0x20 + 8 * consttable.length;
|
|
|
|
for (let i = 0; i < consttable.length; i++) {
|
|
const cv = consttable[i];
|
|
if (typeof cv === 'string') {
|
|
tmpbuf.writeUInt32LE(1);
|
|
tmpbuf.writeUInt32LE(strpos, 4);
|
|
strbuf.asciiWrite(cv + '\0');
|
|
fs.writeSync(cf, strbuf, 0, cv.length + 1, strpos);
|
|
strpos += cv.length + 1;
|
|
strpos = Math.ceil(strpos / 4) * 4;
|
|
} else {
|
|
console.warn('Warning: integer constant (' + cv + ') stored in const table');
|
|
tmpbuf.writeUInt32LE(7);
|
|
tmpbuf.writeUInt32LE(cv, 4);
|
|
}
|
|
fs.writeSync(cf, tmpbuf, 0, 8, 0x20 + i * 8);
|
|
}
|
|
fs.closeSync(cf);
|
|
|
|
// ──────────────────────────────────────────
|
|
// 4. Re-number character IDs
|
|
// ──────────────────────────────────────────
|
|
const aptroot = $('aptdata');
|
|
$('movieclip').attr('id', 0);
|
|
let characters = aptroot.children();
|
|
|
|
// Sort by id
|
|
const charArray = [];
|
|
characters.each(function () { charArray.push(this); });
|
|
charArray.sort(function (a, b) {
|
|
const ida = isNaN(a.attribs.id) ? a.attribs.id : eval(a.attribs.id);
|
|
const idb = isNaN(b.attribs.id) ? b.attribs.id : eval(b.attribs.id);
|
|
if (ida > idb) return 1;
|
|
if (ida < idb) return -1;
|
|
throw new Error('Duplicate ids: ' + a.attribs.id);
|
|
});
|
|
|
|
// Re-attach sorted
|
|
charArray.forEach(function (el) { aptroot.append(el); });
|
|
|
|
const refs = aptroot.find('[character][character!="-1"],[font][font!="-1"],[sprite][sprite!="-1"]');
|
|
charArray.forEach(function (v, i) {
|
|
const $v = $(v);
|
|
const oldid = $v.attr('id');
|
|
const newid = i;
|
|
if ($v.attr('id') !== undefined) {
|
|
$v.attr('id', newid);
|
|
}
|
|
|
|
refs.filter('[character=' + oldid + ']').not('[filtered]').attr({ character: newid, filtered: '1' });
|
|
refs.filter('[font=' + oldid + ']').not('[filtered]').attr({ font: newid, filtered: '1' });
|
|
refs.filter('[sprite=' + oldid + ']').not('[filtered]').attr({ sprite: newid, filtered: '1' });
|
|
});
|
|
|
|
// Check for unresolved references
|
|
let unsolvedref = null;
|
|
refs.each(function () {
|
|
if (!$(this).attr('filtered')) {
|
|
unsolvedref = this;
|
|
return false;
|
|
}
|
|
});
|
|
if (unsolvedref) {
|
|
throw new Error('Reference not found: ' + $.html(unsolvedref));
|
|
}
|
|
|
|
// ──────────────────────────────────────────
|
|
// 5. Resolve jump targets
|
|
// ──────────────────────────────────────────
|
|
actionContainers.each(function () {
|
|
U.calcaln($(this).children('[action]'));
|
|
});
|
|
|
|
const gotos = $('[targetlabel][action]');
|
|
const dups = [];
|
|
gotos.each(function () {
|
|
const v = $(this);
|
|
const action = parseInt(v.attr('action'), 10);
|
|
const pos = parseInt(v.attr('pos'), 10);
|
|
const targetlabel = v.attr('targetlabel');
|
|
const list = v.parent().children();
|
|
const target = list.filter('[mylabel=' + targetlabel + ']');
|
|
|
|
if (target.length > 1) {
|
|
dups.push(targetlabel);
|
|
}
|
|
if (target.is('end')) {
|
|
const pr = target.prevAll('[action]:first');
|
|
const alnp = parseInt(pr.attr('aln'), 10);
|
|
target.attr('aln', alnp + U.alnsize(pr, alnp));
|
|
}
|
|
const nextaln = parseInt(v.nextAll('[action]:first').attr('aln'), 10);
|
|
const targetaln = parseInt(target.attr('aln'), 10);
|
|
v.attr('pos', targetaln - nextaln);
|
|
});
|
|
if (dups.length > 0) {
|
|
throw new Error('Duplicate labels: ' + dups.join(','));
|
|
}
|
|
|
|
// ──────────────────────────────────────────
|
|
// 6. Resolve function sizes
|
|
// ──────────────────────────────────────────
|
|
const functions = $('definefunction,definefunction2');
|
|
functions.each(function () {
|
|
const v = $(this);
|
|
let n = v.next();
|
|
let scope = 1;
|
|
while (n.length) {
|
|
if (n[0].tagName.indexOf('definefunction') === 0) {
|
|
scope++;
|
|
} else if (n[0].tagName === 'end') {
|
|
scope--;
|
|
if (!scope) {
|
|
const endn = n.prevAll('[aln]:first');
|
|
const alnend = parseInt(endn.attr('aln'), 10) + U.alnsize(endn, parseInt(endn.attr('aln'), 10));
|
|
const alnbegin = parseInt(v.attr('aln'), 10) + U.alnsize(v, parseInt(v.attr('aln'), 10));
|
|
v.attr('size', alnend - alnbegin);
|
|
return;
|
|
}
|
|
}
|
|
n = n.next();
|
|
}
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 7. Renumber frames
|
|
// ──────────────────────────────────────────
|
|
$('frames').each(function () {
|
|
$(this).children('frame').each(function (j) {
|
|
$(this).attr('id', j);
|
|
$(this).children('framelabel').attr('frame', j);
|
|
});
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 8. Convert symbolic flags back to numeric
|
|
// ──────────────────────────────────────────
|
|
$('placeobject').each(function () {
|
|
$(this).attr('flags', U.flagstovalue($(this).attr('flags') || '', C.PlaceObjectFlags));
|
|
});
|
|
$('buttonrecord').each(function () {
|
|
$(this).attr('flags', U.flagstovalue($(this).attr('flags'), C.ButtonRecordFlags));
|
|
});
|
|
$('clipaction').each(function () {
|
|
const $v = $(this);
|
|
const realflags = U.endian(U.flagstovalue($v.attr('flags'), C.ClipEventFlags), 3);
|
|
const keycode = parseInt($v.attr('keycode'), 10);
|
|
$v.attr('flags', realflags | (keycode << 24));
|
|
$v.removeAttr('keycode');
|
|
});
|
|
$('buttonaction').each(function () {
|
|
const $v = $(this);
|
|
$v.attr('flags', U.flagstovalue($v.attr('flags'), C.ButtonActionFlags));
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 8b. Convert rotm00/rotm01/... format → matrix for placeobject flag calculation
|
|
// The human-readable XML (expand output) uses expanded rotm00/tx/ty format,
|
|
// but the placeobject flag building below needs the compact `matrix` attribute.
|
|
// ──────────────────────────────────────────
|
|
$('*').each(function () {
|
|
const $v = $(this);
|
|
const rm00 = $v.attr('rotm00');
|
|
if (rm00 !== undefined) {
|
|
const rm01 = $v.attr('rotm01') || '0';
|
|
const rm10 = $v.attr('rotm10') || '0';
|
|
const rm11 = $v.attr('rotm11') || '1';
|
|
const tx = $v.attr('tx') || '0';
|
|
const ty = $v.attr('ty') || '0';
|
|
$v.attr('matrix', [rm00, rm01, rm10, rm11, tx, ty].join(','));
|
|
$v.removeAttr('rotm00 rotm01 rotm10 rotm11 tx ty');
|
|
}
|
|
// unknown (packed int) → rgbaadd (only if non-zero)
|
|
const unk = $v.attr('unknown');
|
|
if (unk !== undefined && parseInt(unk, 10) !== 0) {
|
|
const u = parseInt(unk, 10);
|
|
$v.attr('rgbaadd', (u & 0xff) + ',' + ((u >> 8) & 0xff) + ',' +
|
|
((u >> 16) & 0xff) + ',' + ((u >> 24) & 0xff));
|
|
$v.removeAttr('unknown');
|
|
}
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 9. Build placeobject flags
|
|
// ──────────────────────────────────────────
|
|
$('placeobject').each(function () {
|
|
const $v = $(this);
|
|
let flags = parseInt($v.attr('flags'), 10) || 0;
|
|
|
|
// The flags may be symbolic (e.g. "HasCharacter|HasMatrix") — parse it
|
|
if (isNaN(parseInt($v.attr('flags'), 10))) {
|
|
// Symbolic flags: use them directly
|
|
flags = U.flagstovalue($v.attr('flags'), C.PlaceObjectFlags);
|
|
}
|
|
|
|
if ($v.attr('character') === '-1' || $v.attr('character') === undefined) {
|
|
$v.attr('character', '-1');
|
|
flags &= ~C.PlaceObjectFlags.HasCharacter;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasCharacter;
|
|
}
|
|
// Use compact `matrix` attr (converted from rotm00 in step 8b)
|
|
if ($v.attr('matrix') === undefined) {
|
|
$v.attr('matrix', '1,0,0,1,0,0');
|
|
flags &= ~C.PlaceObjectFlags.HasMatrix;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasMatrix;
|
|
}
|
|
// Color transform: presence of rgbamul or rgbaadd
|
|
if ($v.attr('rgbamul') === undefined && $v.attr('rgbaadd') === undefined) {
|
|
$v.attr('rgbamul', '255,255,255,255');
|
|
$v.attr('rgbaadd', '0,0,0,0');
|
|
flags &= ~C.PlaceObjectFlags.HasColorTransform;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasColorTransform;
|
|
}
|
|
if ($v.attr('ratio') === undefined || $v.attr('ratio') === '0') {
|
|
$v.attr('ratio', '0');
|
|
flags &= ~C.PlaceObjectFlags.HasRatio;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasRatio;
|
|
}
|
|
if ($v.attr('clipdepth') === '-1' || $v.attr('clipdepth') === undefined) {
|
|
$v.attr('clipdepth', '-1');
|
|
flags &= ~C.PlaceObjectFlags.HasClipDepth;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasClipDepth;
|
|
}
|
|
if ($v.find('poname').length === 0) {
|
|
flags &= ~C.PlaceObjectFlags.HasName;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasName;
|
|
}
|
|
if ($v.find('clipactions').length === 0) {
|
|
flags &= ~C.PlaceObjectFlags.HasClipAction;
|
|
} else {
|
|
flags |= C.PlaceObjectFlags.HasClipAction;
|
|
}
|
|
$v.attr('flags', flags);
|
|
});
|
|
|
|
// ──────────────────────────────────────────
|
|
// 10. Handle shapes, images, .ru, manifest, .dat
|
|
// ──────────────────────────────────────────
|
|
let newmanifest = "<?xml version='1.0' encoding='utf-8'?>\n" +
|
|
'<AssetDeclaration xmlns="uri:ea.com:eala:asset" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n' +
|
|
'<AptAptData id="' + baseName + '_apt" File="' + baseName + '.apt" />\n' +
|
|
'<AptConstData id="' + baseName + '_const" File="' + baseName + '.const" />\n' +
|
|
'<AptDatData id="' + baseName + '_dat" File="' + baseName + '.dat" />\n';
|
|
|
|
let newdat = '; Created by AptXMLEditor.\n';
|
|
const texcache = {};
|
|
let imgid = aptroot.children().length - 1;
|
|
let texid = 0;
|
|
let ruerror = false;
|
|
const rucache = {};
|
|
|
|
$('shape').each(function () {
|
|
const sp = $(this);
|
|
const sid = sp.attr('id');
|
|
const ru = sp.find('ru');
|
|
let rupath = sp.attr('geometry');
|
|
let text = ru.text().replace(/t@c/g, 'tc');
|
|
text = text.split(/[\r\n]+/g);
|
|
let text2 = '';
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
let txt = text[i].trim();
|
|
if (txt.indexOf('tc') > 0) {
|
|
txt = txt.split(':');
|
|
if (txt[5].indexOf('.') > 0) {
|
|
// filename reference → find or create image
|
|
let iid = $('image').filter(function () {
|
|
return $(this).attr('image').toLowerCase() === txt[5].toLowerCase();
|
|
}).attr('id');
|
|
|
|
if (iid === undefined) {
|
|
// Create new image
|
|
imgid++;
|
|
const newimg = U.el('image', $);
|
|
newimg.attr({ id: imgid, image: txt[5] });
|
|
aptroot.append(newimg);
|
|
iid = imgid;
|
|
}
|
|
txt[5] = iid;
|
|
} else {
|
|
// numeric id reference
|
|
const iid = txt[5];
|
|
if ($('image[id=' + iid + '],empty[id=' + iid + ']').length === 0) {
|
|
throw new Error('Reference not found: ' + rupath + ' => <image id="' + iid + '" ...');
|
|
}
|
|
}
|
|
txt = txt.join(':');
|
|
}
|
|
if (txt !== '') {
|
|
text2 += txt + '\n';
|
|
}
|
|
}
|
|
|
|
const rupathFull = (outDir + rupath).toLowerCase();
|
|
rucache[rupathFull] = text2;
|
|
newmanifest += '<AptGeometryData id="' + baseName + '_' + sid + '" File="' + rupath + '" AptID="' + sid + '"/>\n';
|
|
sp.attr('geometry', sid);
|
|
ru.remove();
|
|
});
|
|
|
|
// Process images
|
|
$('image').each(function () {
|
|
const v = $(this);
|
|
let img1 = v.attr('image');
|
|
const img = img1.toLowerCase();
|
|
const iid = v.attr('id');
|
|
|
|
if (!texcache[img]) {
|
|
let newid;
|
|
if (img.indexOf(',') > 0) {
|
|
newid = iid;
|
|
} else {
|
|
texid++;
|
|
newid = texid;
|
|
}
|
|
texcache[img] = newid;
|
|
newmanifest += '<Texture id="apt_' + baseName + '_' + newid + '" File="' + img1.split(',')[0] + '" OutputFormat="A8R8G8B8" GenerateMipMaps="false" AllowAutomaticResize="false"/>\n';
|
|
}
|
|
|
|
if (img.indexOf(',') > 0) {
|
|
newdat += iid + '=' + img.split(',')[1] + '\n';
|
|
} else {
|
|
newdat += iid + '->' + texcache[img] + '\n';
|
|
}
|
|
v.attr('image', iid);
|
|
});
|
|
|
|
newmanifest += '</AssetDeclaration>\n';
|
|
|
|
// ──────────────────────────────────────────
|
|
// 11. Attribute cleanup & transformation
|
|
// ──────────────────────────────────────────
|
|
$('*').removeAttr('filtered mylabel targetlabel aln newid');
|
|
|
|
// Process each element for attribute conversions
|
|
$('*').each(function () {
|
|
const $v = $(this);
|
|
let t;
|
|
|
|
// matrix → rotm00/rotm01/rotm10/rotm11/tx/ty
|
|
if ((t = $v.attr('matrix'))) {
|
|
const a = t.split(',');
|
|
$v.attr('rotm00', a[0]);
|
|
$v.attr('rotm01', a[1]);
|
|
$v.attr('rotm10', a[2]);
|
|
$v.attr('rotm11', a[3]);
|
|
$v.attr('tx', a[4]);
|
|
$v.attr('ty', a[5]);
|
|
$v.removeAttr('matrix');
|
|
}
|
|
|
|
// rgbamul/rgba → red,green,blue,alpha
|
|
if ((t = $v.attr('rgbamul')) || (t = $v.attr('rgba'))) {
|
|
const rgbam = t.split(',');
|
|
$v.attr('red', rgbam[0]);
|
|
$v.attr('green', rgbam[1]);
|
|
$v.attr('blue', rgbam[2]);
|
|
$v.attr('alpha', rgbam[3]);
|
|
$v.removeAttr('rgbamul rgba');
|
|
}
|
|
|
|
// rgbaadd → unknown (packed int)
|
|
if ((t = $v.attr('rgbaadd'))) {
|
|
const rgbaa = t.split(',');
|
|
$v.attr('unknown', parseInt(rgbaa[0], 10) |
|
|
(parseInt(rgbaa[1], 10) << 8) |
|
|
(parseInt(rgbaa[2], 10) << 16) |
|
|
(parseInt(rgbaa[3], 10) << 24));
|
|
$v.removeAttr('rgbaadd');
|
|
}
|
|
|
|
// color (comma-separated) → packed int
|
|
if ((t = $v.attr('color'))) {
|
|
const rgba = t.split(',');
|
|
$v.attr('color', parseInt(rgba[0], 10) |
|
|
(parseInt(rgba[1], 10) << 8) |
|
|
(parseInt(rgba[2], 10) << 16) |
|
|
(parseInt(rgba[3], 10) << 24));
|
|
}
|
|
|
|
// movieclip: framedelay → unknown
|
|
if ($v.is('movieclip') && $v.attr('framedelay') !== undefined) {
|
|
$v.attr('unknown', $v.attr('framedelay'));
|
|
$v.removeAttr('framedelay');
|
|
}
|
|
});
|
|
|
|
// Swap red/blue for edittext
|
|
$('edittext').each(function () {
|
|
const $v = $(this);
|
|
const r = $v.attr('red');
|
|
const b = $v.attr('blue');
|
|
$v.attr('red', b);
|
|
$v.attr('blue', r);
|
|
});
|
|
|
|
// Remove end/dummy markers
|
|
$('end,dummy').remove();
|
|
|
|
// Serialize
|
|
let output = $.xml();
|
|
output = output.replace(/^[\t]+$/gm, '').replace(/[\r\n]+/g, '\n');
|
|
|
|
return {
|
|
rawXml: output,
|
|
manifest: newmanifest,
|
|
dat: newdat,
|
|
ruCache: rucache
|
|
};
|
|
}
|
|
|
|
// ─── stringtoaction(): text assembly → XML action node ───
|
|
function stringtoaction(s, ActionNames, $) {
|
|
s = s.trim();
|
|
if (!s) return null;
|
|
|
|
// Comment line
|
|
if (s[0] === ';') {
|
|
return s;
|
|
}
|
|
|
|
let m;
|
|
|
|
// definefunction2: function name(args)(regs:N, reg1:this, ...)
|
|
if ((m = s.match(/^function[\s]+([\w]*)\(([^)]*)\)[\s]*\(([^)]*)\)/i))) {
|
|
const a = U.el('definefunction2', $);
|
|
a.attr('name', m[1] || '');
|
|
a.attr('action', C.ACTION_DEFINEFUNCTION2);
|
|
a.attr('size', 0);
|
|
|
|
const args = m[2].replace(/\s/g, '');
|
|
const args2 = m[3].replace(/\s/g, '');
|
|
const sysa = [...C.SYSTEM_ARGUMENTS];
|
|
const sysab = [...C.SYSTEM_ARGUMENTS];
|
|
let beginsys = false;
|
|
let lastsys = 0;
|
|
let realflags = C.FunctionPreloadFlags.SupressThis |
|
|
C.FunctionPreloadFlags.SupressArguments |
|
|
C.FunctionPreloadFlags.SupressSuper;
|
|
let nregs = 20;
|
|
|
|
if (args2) {
|
|
const parts = args2.split(',');
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (parts[i]) {
|
|
const am = parts[i].match(/^reg([s]|[0-9]+):([\w]+)/i);
|
|
if (am) {
|
|
const ar = am[1];
|
|
const an = am[2];
|
|
if (ar[0] === 's') {
|
|
nregs = parseInt(an, 10);
|
|
continue;
|
|
} else {
|
|
const arNum = parseInt(ar, 10);
|
|
if (sysa.indexOf(an) >= 0) {
|
|
if (!beginsys) {
|
|
if (arNum !== 1) return bad('system args must start from reg1', $);
|
|
beginsys = true;
|
|
}
|
|
lastsys++;
|
|
if (arNum !== lastsys) return bad('system args must be sequential', $);
|
|
while (sysa.shift() !== an) { /* empty */ }
|
|
const fp = C.FunctionPreloadFlags;
|
|
if (an === 'this') { realflags |= fp.PreloadThis; realflags &= ~fp.SupressThis; }
|
|
else if (an === 'arguments') { realflags |= fp.PreloadArguments; realflags &= ~fp.SupressArguments; }
|
|
else if (an === 'super') { realflags |= fp.PreloadSuper; realflags &= ~fp.SupressSuper; }
|
|
else if (an === '_root') { realflags |= fp.PreloadRoot; }
|
|
else if (an === '_parent') { realflags |= fp.PreloadParent; }
|
|
else if (an === '_global') { realflags |= fp.PreloadGlobal; }
|
|
else if (an === '_extern') { realflags |= fp.PreloadExtern; }
|
|
} else if (sysab.indexOf(an) >= 0) {
|
|
return bad('system argument in wrong order', $);
|
|
} else {
|
|
return bad('unknown argument type', $);
|
|
}
|
|
}
|
|
} else {
|
|
return bad('wrong argument format', $);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle normal arguments (first part of the declaration)
|
|
if (args) {
|
|
const argParts = args.split(',');
|
|
for (let i = 0; i < argParts.length; i++) {
|
|
if (argParts[i]) {
|
|
const am = argParts[i].match(/^reg([0-9]+):([\w]+)/i);
|
|
if (am) {
|
|
const an = U.el('argument', $);
|
|
an.attr('reg', am[1]);
|
|
an.attr('name', am[2]);
|
|
a.append(an);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const flagsVal = (U.endian(realflags, 3) << 8) | (nregs & 0xff);
|
|
a.attr('flags', flagsVal);
|
|
return a;
|
|
}
|
|
|
|
// definefunction: function name(args)
|
|
if ((m = s.match(/^function[\s]+([\w]*)\(([^)]*)\)/i))) {
|
|
const a = U.el('definefunction', $);
|
|
a.attr('name', m[1] || '');
|
|
a.attr('action', C.ACTION_DEFINEFUNCTION);
|
|
a.attr('size', 0);
|
|
const args = m[2].replace(/\s/g, '');
|
|
if (args) {
|
|
const argParts = args.split(',');
|
|
for (let i = 0; i < argParts.length; i++) {
|
|
if (argParts[i]) {
|
|
const an = U.el('argument', $);
|
|
an.attr('name', argParts[i].trim());
|
|
a.append(an);
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// label: (starts with ; or continues from previous line)
|
|
if (m = s.match(/^([\w]+)[\s]*:$/)) {
|
|
return m[1]; // return label name WITHOUT colon (caller uses it as mylabel)
|
|
}
|
|
|
|
// end of function
|
|
if (s === 'end') {
|
|
return U.el('end', $);
|
|
}
|
|
|
|
// Parse instruction: actionname [args...]
|
|
// Split into instruction name and arguments
|
|
const parts = s.match(/([\w]+)(?:\s+(.*))?/);
|
|
if (!parts) return null;
|
|
|
|
const instrName = parts[1];
|
|
const argStr = parts[2] ? parts[2].trim() : '';
|
|
|
|
// Look up the action code by name
|
|
let aid = -1;
|
|
// Prefer the current ActionNames table (might be old or new style)
|
|
for (let i = 0; i < ActionNames.length; i++) {
|
|
if (ActionNames[i] && ActionNames[i].toLowerCase() === instrName.toLowerCase()) {
|
|
aid = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If not found in current style, search both tables
|
|
if (aid < 0) {
|
|
for (let i = 0; i < C.ActionNames.length; i++) {
|
|
if (C.ActionNames[i] && C.ActionNames[i].toLowerCase() === instrName.toLowerCase()) {
|
|
aid = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (aid < 0) {
|
|
for (let i = 0; i < C.ActionNames2.length; i++) {
|
|
if (C.ActionNames2[i] && C.ActionNames2[i].toLowerCase() === instrName.toLowerCase()) {
|
|
aid = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (aid < 0) {
|
|
// Check for plain action name like "push" which might map to specific actions
|
|
return bad('unknown instruction: ' + instrName, $);
|
|
}
|
|
|
|
// Parse arguments based on action type
|
|
const args = argStr ? parseArgs(argStr) : [];
|
|
|
|
// Branch instructions: instr labelname
|
|
if (aid === C.ACTION_BRANCHALWAYS || aid === C.ACTION_BRANCHIFTRUE || aid === C.EA_BRANCHIFFALSE) {
|
|
const a = U.el('goto', $);
|
|
a.attr('action', aid);
|
|
a.attr('targetlabel', argStr);
|
|
return a;
|
|
}
|
|
|
|
// goto frame
|
|
if (aid === C.ACTION_GOTOFRAME || aid === C.ACTION_SETREGISTER ||
|
|
aid === C.ACTION_WITH || aid === C.ACTION_GOTOEXPRESSION) {
|
|
const a = U.el('goto', $);
|
|
a.attr('action', aid);
|
|
a.attr('pos', parseInt(argStr, 10) || 0);
|
|
return a;
|
|
}
|
|
|
|
// geturl: instr "url", "target"
|
|
if (aid === C.ACTION_GETURL) {
|
|
const a = U.el('geturl', $);
|
|
a.attr('action', aid);
|
|
a.attr('str1', args[0] || '');
|
|
a.attr('str2', args[1] || '');
|
|
return a;
|
|
}
|
|
|
|
// pushdata: instr val1, val2, ...
|
|
if (aid === C.ACTION_PUSHDATA) {
|
|
const a = U.el('pushdata', $);
|
|
a.attr('action', aid);
|
|
for (let i = 0; i < args.length; i++) {
|
|
const c = U.el('constant', $);
|
|
const numVal = parseInt(args[i], 10);
|
|
if (isNaN(numVal) || args[i] === '') {
|
|
c.attr('str', args[i] || '');
|
|
c.attr('val', 0);
|
|
} else {
|
|
c.attr('num', numVal);
|
|
c.attr('val', 0);
|
|
}
|
|
a.append(c);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// String-type instructions: instr "string"
|
|
if (aid === C.EA_PUSHSTRING || aid === C.EA_GETSTRINGVAR ||
|
|
aid === C.EA_GETSTRINGMEMBER || aid === C.EA_SETSTRINGVAR ||
|
|
aid === C.EA_SETSTRINGMEMBER || aid === C.ACTION_SETTARGET ||
|
|
aid === C.ACTION_GOTOLABEL) {
|
|
const a = U.el('string', $);
|
|
a.attr('action', aid);
|
|
a.attr('str', args[0] || argStr.replace(/^"|"$/g, ''));
|
|
a.attr('val', 0);
|
|
return a;
|
|
}
|
|
|
|
// Constant-referencing: instr "str" or instr num
|
|
if (aid === C.EA_CALLNAMEDFUNCTIONPOP || aid === C.EA_CALLNAMEDFUNCTION ||
|
|
aid === C.EA_CALLNAMEDMETHODPOP || aid === C.EA_CALLNAMEDMETHOD ||
|
|
aid === C.EA_PUSHCONSTANT || aid === C.EA_GETNAMEDMEMBER ||
|
|
aid === C.EA_PUSHVALUEOFVAR) {
|
|
const a = U.el('byte', $);
|
|
a.attr('action', aid);
|
|
// Check if the original argument was quoted (indicates a string constant)
|
|
// e.g. pushconst "1" is a string, pushconst 1 is a number
|
|
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);
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// pushbyte and pushregister take a byte
|
|
if (aid === C.EA_PUSHBYTE || aid === C.EA_PUSHREGISTER) {
|
|
const a = U.el('byte', $);
|
|
a.attr('action', aid);
|
|
a.attr('val', parseInt(args[0], 10) || 0);
|
|
return a;
|
|
}
|
|
|
|
// pushwordconstant
|
|
if (aid === C.EA_PUSHWORDCONSTANT) {
|
|
const a = U.el('short', $);
|
|
a.attr('action', aid);
|
|
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);
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// pushshort
|
|
if (aid === C.EA_PUSHSHORT) {
|
|
const a = U.el('short', $);
|
|
a.attr('action', aid);
|
|
a.attr('val', parseInt(args[0], 10) || 0);
|
|
return a;
|
|
}
|
|
|
|
// pushfloat / pushlong
|
|
if (aid === C.EA_PUSHFLOAT || aid === C.EA_PUSHLONG) {
|
|
const tag = aid === C.EA_PUSHFLOAT ? 'float' : 'long';
|
|
const a = U.el(tag, $);
|
|
a.attr('action', aid);
|
|
if (aid === C.EA_PUSHFLOAT && args[0]) {
|
|
// Float is stored as a 4-byte value via parseInt
|
|
a.attr('val', parseFloat(args[0]));
|
|
} else {
|
|
a.attr('val', parseInt(args[0], 10) || 0);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// All other opcodes that have no arguments
|
|
if (!args.length || (args.length === 1 && args[0] === '')) {
|
|
const a = U.el('noarg', $);
|
|
a.attr('action', aid);
|
|
return a;
|
|
}
|
|
// Unknown action with unexpected args — make a noarg with val anyway
|
|
const a = U.el('noarg', $);
|
|
a.attr('action', aid);
|
|
a.attr('val', parseInt(args[0], 10) || 0);
|
|
return a;
|
|
}
|
|
|
|
// ─── Parse argument string ───
|
|
function parseArgs(argStr) {
|
|
const args = [];
|
|
let current = '';
|
|
let inQuote = false;
|
|
|
|
for (let i = 0; i < argStr.length; i++) {
|
|
const c = argStr[i];
|
|
if (c === '"') {
|
|
inQuote = !inQuote;
|
|
current += c;
|
|
} else if (c === ',' && !inQuote) {
|
|
args.push(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += c;
|
|
}
|
|
}
|
|
if (current.trim()) {
|
|
args.push(current.trim());
|
|
}
|
|
|
|
// Strip surrounding quotes from each argument (matching original eval() behavior)
|
|
return args.map(s => {
|
|
const t = s.trim();
|
|
if (t.length >= 2 && t[0] === '"' && t[t.length - 1] === '"') {
|
|
return t.slice(1, -1);
|
|
}
|
|
return t;
|
|
});
|
|
}
|
|
|
|
// ─── bad() — create error marker element ───
|
|
function bad(msg, $) {
|
|
return $('<bad>').attr('text', msg);
|
|
}
|
|
|
|
// ─── Exports ───
|
|
module.exports = { build };
|