'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 = "\n" + '\n' + '\n' + '\n' + '\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 + ' => \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 += '\n'; } if (img.indexOf(',') > 0) { newdat += iid + '=' + img.split(',')[1] + '\n'; } else { newdat += iid + '->' + texcache[img] + '\n'; } v.attr('image', iid); }); newmanifest += '\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 $('').attr('text', msg); } // ─── Exports ─── module.exports = { build };