Fix APT CLI roundtrip visual and script preservation

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.
This commit is contained in:
2026-10-13 02:40:37 +02:00
parent 5511354783
commit e0b8dedc04
18 changed files with 5111 additions and 115 deletions
@@ -0,0 +1,82 @@
'use strict';
const test = require('node:test');
const H = require('./public-cli-helper.cjs');
function rawAction(nodes) {
return `<aptdata>
<movieclip id="0" screensizex="1365" screensizey="768" unknown="33">
<imports/><exports/>
<frames><frame id="0"><action>
${nodes}
<noarg action="0"/>
</action></frame></frames>
</movieclip>
</aptdata>`;
}
test('public expand/build preserves an edited supported pushfloat without integer truncation', () => {
const work = H.testCase('pushfloat');
const source = H.expanded(work, rawAction('<float action="180" val="85.25"/>'));
H.assert.match(source.xml, /pushfloat 85\.25/);
const edited = source.xml.replace('pushfloat 85.25', 'pushfloat 24.5');
H.assert.notEqual(edited, source.xml);
const rebuilt = H.build(work, edited);
H.succeeded(rebuilt.result);
H.assert.equal(Number(H.element(rebuilt.raw, 'float', 'action', 180).val), 24.5);
});
test('public expand/build distinguishes a numeric-looking quoted string from an integer pool value', () => {
const work = H.testCase('quoted-numeric');
const source = H.expanded(work, rawAction(`<constantpool action="136">
<constant val="0" str="007"/>
<constant val="1" num="7"/>
</constantpool>
<byte action="162" val="0"/>
<byte action="162" val="1"/>`));
H.assert.match(source.xml, /pushconst "007"/);
H.assert.match(source.xml, /pushconst 7(?:\s|<)/);
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.equal(H.element(rebuilt.raw, 'constant', 'str', '007').str, '007');
H.assert.equal(H.element(rebuilt.raw, 'constant', 'num', '7').num, '7');
// This .const was generated by production build(), not by the fake converter.
const table = H.fs.readFileSync(rebuilt.output.replace(/\.apt$/, '.const'));
H.assert.equal(table.readUInt32LE(0x18), 2);
const values = [];
for (let i = 0; i < 2; i++) {
const offset = 0x20 + i * 8;
const type = table.readUInt32LE(offset);
const value = table.readUInt32LE(offset + 4);
values.push(type === 1 ? { type: 'string', value: table.subarray(value, table.indexOf(0, value)).toString('ascii') } :
{ type: 'integer', value });
}
H.assert.deepEqual(values, [{ type: 'string', value: '007' }, { type: 'integer', value: 7 }]);
});
test('public pushdata build retains quoted "007" and integer 7 as data nodes and correctly typed CONST values', () => {
const work = H.testCase('pushdata-types');
const source = H.expanded(work, rawAction(`<pushdata action="150">
<data val="0" str="007"/>
<data val="1" num="7"/>
</pushdata>`));
H.assert.match(source.xml, /pushdata "007", 7/);
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
const body = rebuilt.raw.match(/<pushdata\b[^>]*>([\s\S]*?)<\/pushdata>/);
H.assert.ok(body);
const children = Array.from(body[1].matchAll(/<(data|constant)\b[^>]*>/g), match =>
({ tag: match[1], attrs: H.attributes(match[0]) }));
H.assert.equal(children.length, 2);
H.assert.deepEqual(children.map(child => child.tag), ['data', 'data'],
'xml2apt expects data children; constant children previously crashed the native converter');
H.assert.equal(children[0].attrs.str, '007');
H.assert.equal(children[0].attrs.num, undefined);
H.assert.equal(Number(children[1].attrs.num), 7);
H.assert.equal(children[1].attrs.str, undefined);
const records = H.constRecords(rebuilt.output);
H.assert.deepEqual(records.map(record => ({ type: record.type, value: record.value })),
[{ type: 1, value: '007' }, { type: 7, value: 7 }]);
H.assert.equal(Number(children[0].attrs.val), 0);
H.assert.equal(Number(children[1].attrs.val), 1);
});