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
+12 -44
View File
@@ -26,7 +26,7 @@ function expand(input, output, opts) {
const rawXml = U.readRawXml(tmpdir, baseName);
// 4. Parse manifest if available (for texture name resolution)
const manifest = manifestData ? cheerio.load(manifestData, { xmlMode: true, decodeEntities: false }) : null;
const manifest = manifestData ? cheerio.load(manifestData, { xmlMode: true, decodeEntities: true }) : null;
// 5. Parse .dat if available
const dats = parseDat(datData);
@@ -69,20 +69,11 @@ function parseDat(datData) {
function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle) {
U.resetLabels();
const $ = cheerio.load(rawXml, { xmlMode: true, decodeEntities: false });
const $ = cheerio.load(rawXml, { xmlMode: true, decodeEntities: true });
const ActionNames = oldStyle ? C.ActionNames2 : C.ActionNames;
const xmldoc = $;
// Remove duplicated exports
const exp = {};
$('export').each(function () {
const nm = $(this).attr('name');
if (exp[nm]) {
$(this).remove();
} else {
exp[nm] = true;
}
});
// Preserve export order and repeated names, including Object prototype names.
// Calculate aligned positions for each action container
const actionContainers = $('initaction, buttonaction, action, clipaction');
@@ -179,8 +170,8 @@ function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle
}
});
astr += U.indentstr(v);
// Replace CDATA with text node (entity-encoded)
v.empty().text(astr);
// Parse an XML CDATA node so serialization does not entity-encode assembly text.
v.empty().append('<![CDATA[' + astr.replace(/\]\]>/g, ']]]]><![CDATA[>') + ']]>');
});
// Resolve flags to symbolic names
@@ -268,10 +259,10 @@ function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle
$v.removeAttr('character');
}
if (flags.indexOf('HasMatrix') < 0) {
$v.removeAttr('matrix');
$v.removeAttr('matrix rotm00 rotm01 rotm10 rotm11 tx ty');
}
if (flags.indexOf('HasColorTransform') < 0) {
$v.removeAttr('rgbamul rgbaadd');
$v.removeAttr('rgbamul rgbaadd red green blue alpha unknown');
}
if (flags.indexOf('HasRatio') < 0) {
$v.removeAttr('ratio');
@@ -287,12 +278,7 @@ function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle
}
});
// Clean up: remove extra attributes from the final output
$('edittext').each(function () {
const $v = $(this);
// Override for xmladdhint: store original color info
// (the original code swaps red/blue for edittext display - preserve that)
});
// Keep edittext channels in cc3tools' order; build uses the same order.
// Clean internal attributes
$('*').each(function () {
@@ -302,29 +288,11 @@ function xmladdhint(rawXml, baseName, aptName, manifest, dats, filedir, oldStyle
// Serialize
let output = $.xml();
// Wrap action container text content in CDATA sections
// This prevents cheerio from entity-encoding & in the action assembly text
output = output.replace(
/<(initaction|buttonaction|action|clipaction)(\s[^>]*)?>([\s\S]*?)<\/\1>/g,
(match, tag, attrs, content) => {
const trimmed = content.trim();
if (!trimmed) return match;
return '<' + tag + (attrs || '') + '><![CDATA[' + content + ']]></' + tag + '>';
}
);
// Clean up excessive whitespace
output = output.replace(/^[\t]+$/gm, '').replace(/[\r\n]+/g, '\n');
return output;
}
// ─── Decode XML entities in attribute values ───
// With decodeEntities: false in cheerio load, attribute values preserve XML entities
// (e.g. &amp; stays as &amp;). We need to decode them for human-readable output.
function decodeAttrEntities(s) {
if (typeof s !== 'string') return s;
return s.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
}
// ─── actiontostring(): XML action node → text assembly ───
function actiontostring(a, ActionNames, $) {
const action = parseInt(a.attr('action'), 10);
@@ -347,7 +315,7 @@ function actiontostring(a, ActionNames, $) {
action === C.EA_CALLNAMEDFUNCTION || action === C.EA_CALLNAMEDMETHODPOP ||
action === C.EA_CALLNAMEDMETHOD || action === C.EA_PUSHVALUEOFVAR ||
action === C.EA_GETNAMEDMEMBER || action === C.EA_PUSHWORDCONSTANT) {
return aname + ' ' + (a.attr('str') !== undefined ? U.mkstr(decodeAttrEntities(a.attr('str'))) : a.attr('num'));
return aname + ' ' + (a.attr('str') !== undefined ? U.mkstr(a.attr('str')) : a.attr('num'));
}
// goto: ActionGotoFrame pos
@@ -357,7 +325,7 @@ function actiontostring(a, ActionNames, $) {
// geturl
if (a[0].tagName === 'geturl') {
return aname + ' ' + U.mkstr(decodeAttrEntities(a.attr('str1'))) + ', ' + U.mkstr(decodeAttrEntities(a.attr('str2')));
return aname + ' ' + U.mkstr(a.attr('str1')) + ', ' + U.mkstr(a.attr('str2'));
}
// definefunction
@@ -408,7 +376,7 @@ function actiontostring(a, ActionNames, $) {
// string instruction: pushstr "hello"
if (a[0].tagName === 'string') {
return aname + ' ' + U.mkstr(decodeAttrEntities(a.attr('str')));
return aname + ' ' + U.mkstr(a.attr('str'));
}
// pushdata: pushdata val1, val2, ...
@@ -416,7 +384,7 @@ function actiontostring(a, ActionNames, $) {
const vals = [];
a.children().each(function () {
const v = $(this);
vals.push(v.attr('str') !== undefined ? U.mkstr(decodeAttrEntities(v.attr('str'))) : v.attr('num'));
vals.push(v.attr('str') !== undefined ? U.mkstr(v.attr('str')) : v.attr('num'));
});
return aname + ' ' + vals.join(', ');
}