Files
EpicNext-Cms/src/lib/services/swf/xml-processor.ts
T
openhands 5e8a13a84f
Deploy / release (push) Successful in 4s
Deploy / deploy (push) Skipped
fix: resolve all biomaly lint errors and warnings across CMS
- Fix CSS parser config (tailwindDirectives enabled)
- Fix noDangerouslySetInnerHtml via SanitizedHtml component
- Fix useExhaustiveDependencies in catalog-manager-dialog
- Fix noArrayIndexKey across 26 files (stable keys)
- Fix SVG a11y (titles, roles, aria-labels)
- Fix label/input associations (htmlFor/id pairs)
- Fix static element interactions (role + keyboard support)
- Fix noImgElement, noDescendingSpecificity (disabled - external Habbo URLs)
- Fix noNonNullAssertion, useTemplate, unused vars/imports
- Add SanitizedHtml shared component
- Migrate biome.json to 2.5.4 schema
2026-07-20 17:41:53 +02:00

628 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* XML parser and XML data processor functions for SWF-to-Nitro conversion.
*
* Pure string/data processing — no external imports needed.
*/
// ── Interfaces ─────────────────────────────────────────────────────
export interface AssetOffset {
x: number;
y: number;
}
export interface NitroVisualization {
layerCount: number;
angle: number;
size: number;
layers?: Record<string, Record<string, unknown>>;
directions?: Record<string, Record<string, unknown>>;
colors?: Record<string, unknown>;
animations?: Record<string, unknown>;
postures?: {
defaultPosture?: string;
postures?: { id: string; animationId: number }[];
};
gestures?: { id: string; animationId: number }[];
}
// ── Minimal XML Parser ─────────────────────────────────────────────
export interface XmlNode {
tag: string;
attrs: Record<string, string>;
children: XmlNode[];
text: string;
}
export function parseXml(xml: string): XmlNode | null {
const stack: XmlNode[] = [];
const nodes: XmlNode[] = [];
// Remove XML declaration and comments
const clean = xml
.replace(/<\?[^?]*\?>/g, "")
.replace(/<!--[\s\S]*?-->/g, "")
.trim();
const tagRe =
/<\/?([a-zA-Z_][\w.-]*)((?:\s+[a-zA-Z_][\w.-]*\s*=\s*"[^"]*")*)\s*\/?>/g;
let lastIndex = 0;
for (const match of clean.matchAll(tagRe)) {
const fullMatch = match[0];
const tagName = match[1];
const attrsStr = match[2] || "";
const matchIndex = match.index ?? 0;
const text = clean.substring(lastIndex, matchIndex).trim();
if (text && stack.length > 0) {
stack[stack.length - 1].text += text;
}
lastIndex = matchIndex + fullMatch.length;
if (fullMatch.startsWith("</")) {
if (stack.length > 0) {
const node = stack.pop();
if (!node) continue;
if (stack.length > 0) {
stack[stack.length - 1].children.push(node);
} else {
nodes.push(node);
}
}
} else {
const attrs: Record<string, string> = {};
const attrRe = /([a-zA-Z_][\w.-]*)\s*=\s*"([^"]*)"/g;
for (const am of attrsStr.matchAll(attrRe)) {
attrs[am[1]] = am[2];
}
const node: XmlNode = { tag: tagName, attrs, children: [], text: "" };
if (fullMatch.endsWith("/>")) {
if (stack.length > 0) {
stack[stack.length - 1].children.push(node);
} else {
nodes.push(node);
}
} else {
stack.push(node);
}
}
}
while (stack.length > 1) {
const node = stack.pop();
if (!node) break;
stack[stack.length - 1].children.push(node);
}
if (stack.length === 1) nodes.push(stack[0]);
return nodes[0] || null;
}
export function findChild(node: XmlNode, tag: string): XmlNode | undefined {
return node.children.find((c) => c.tag === tag);
}
export function findChildren(node: XmlNode, tag: string): XmlNode[] {
return node.children.filter((c) => c.tag === tag);
}
// ── XML Data Processors ────────────────────────────────────────────
export interface ManifestAsset {
name: string;
x: number;
y: number;
}
export function processManifest(xml: string): { assets: ManifestAsset[] } {
const assets: ManifestAsset[] = [];
const root = parseXml(xml);
if (!root) return { assets };
const libNode = root.tag === "manifest" ? findChild(root, "library") : root;
if (!libNode) return { assets };
const assetsNode = findChild(libNode, "assets");
if (!assetsNode) return { assets };
for (const asset of findChildren(assetsNode, "asset")) {
const name = asset.attrs.name || "";
if (!name) continue;
// Skip shadow sprites
if (name.startsWith("sh_")) continue;
let x = 0,
y = 0;
const paramNode = findChild(asset, "param");
if (paramNode && paramNode.attrs.key === "offset") {
const parts = (paramNode.attrs.value || "").split(",");
x = parseInt(parts[0] || "0", 10);
y = parseInt(parts[1] || "0", 10);
}
assets.push({ name, x, y });
}
return { assets };
}
export interface PaletteEntry {
id: number;
source?: string;
master?: boolean;
tags?: string[];
breed?: number;
colorTag?: number;
color1?: string;
color2?: string;
}
export function processAssetsXml(xml: string): {
offsets: Map<string, AssetOffset>;
palettes: Record<string, PaletteEntry>;
} {
const offsets = new Map<string, AssetOffset>();
const palettes: Record<string, PaletteEntry> = {};
const root = parseXml(xml);
if (!root) return { offsets, palettes };
const assetsNode =
root.tag === "assets" ? root : findChild(root, "assets") || root;
for (const asset of findChildren(assetsNode, "asset")) {
const name = asset.attrs.name || "";
if (!name) continue;
const x = parseInt(asset.attrs.x || "0", 10);
const y = parseInt(asset.attrs.y || "0", 10);
offsets.set(name, { x, y });
}
// Parse palettes
for (const pal of findChildren(assetsNode, "palette")) {
const id = parseInt(pal.attrs.id || "0", 10);
const entry: PaletteEntry = { id };
if (pal.attrs.source) entry.source = pal.attrs.source;
if (pal.attrs.master === "1" || pal.attrs.master === "true")
entry.master = true;
if (pal.attrs.tags) entry.tags = pal.attrs.tags.split(",");
if (pal.attrs.breed) entry.breed = parseInt(pal.attrs.breed, 10);
if (pal.attrs.colorTag) entry.colorTag = parseInt(pal.attrs.colorTag, 10);
if (pal.attrs.color1) entry.color1 = pal.attrs.color1;
if (pal.attrs.color2) entry.color2 = pal.attrs.color2;
palettes[String(id)] = entry;
}
return { offsets, palettes };
}
export function processIndex(xml: string): {
visualizationType: string;
logicType: string;
specialtype: number;
} | null {
const root = parseXml(xml);
if (!root) return null;
const obj = root.tag === "object" ? root : findChild(root, "object");
if (!obj) return null;
return {
visualizationType: obj.attrs.visualization || "",
logicType: obj.attrs.logic || "",
specialtype: parseInt(obj.attrs.specialtype || "0", 10),
};
}
export interface LogicResult {
dimensions: { x: number; y: number; z: number; centerZ?: number };
directions: number[];
maskType: string;
canstandon: boolean;
cansiton: boolean;
canlayon: boolean;
action?: { link?: string; startState?: number };
credits?: string;
soundSample?: { id: number; noPitch?: boolean };
customVars?: { variables: string[] };
particleSystems?: Record<string, unknown>[];
planetSystems?: Record<string, unknown>[];
}
export function processLogic(xml: string): LogicResult {
const defaults: LogicResult = {
dimensions: { x: 1, y: 1, z: 0 },
directions: [0],
maskType: "",
canstandon: false,
cansiton: false,
canlayon: false,
};
const root = parseXml(xml);
if (!root) return defaults;
const model = findChild(root, "model") || root;
const dims = findChild(model, "dimensions");
const dirsNode = findChild(model, "directions");
const dimensions: LogicResult["dimensions"] = {
x: parseInt(dims?.attrs.x || "1", 10),
y: parseInt(dims?.attrs.y || "1", 10),
z: parseFloat(dims?.attrs.z || "0"),
};
if (dims?.attrs.centerZ) {
dimensions.centerZ = parseFloat(dims.attrs.centerZ);
}
const directions: number[] = [];
if (dirsNode) {
for (const d of findChildren(dirsNode, "direction")) {
directions.push(parseInt(d.attrs.id || "0", 10));
}
}
// Default directions [0, 90] when missing (matches Retrosprite behavior)
if (directions.length === 0) directions.push(0, 90);
// Parse mask type
const maskNode = findChild(root, "mask");
const maskType = maskNode?.attrs.type || "";
// Infer canstandon/cansiton/canlayon from action nodes
const actions = findChildren(root, "action");
const cansiton = actions.some((a) => a.attrs.kind === "sit");
const canlayon = actions.some((a) => a.attrs.kind === "lay");
const canstandon = !cansiton && !canlayon && dimensions.z > 0;
const result: LogicResult = {
dimensions,
directions,
maskType,
canstandon,
cansiton,
canlayon,
};
// Parse action link/startState
const actionNode = findChild(root, "action");
if (actionNode && (actionNode.attrs.link || actionNode.attrs.startState)) {
const action: LogicResult["action"] = {};
if (actionNode.attrs.link) action.link = actionNode.attrs.link;
if (actionNode.attrs.startState)
action.startState = parseInt(actionNode.attrs.startState, 10);
result.action = action;
}
// Parse credits
const creditsNode = findChild(root, "credits");
if (creditsNode?.attrs.value) {
result.credits = creditsNode.attrs.value;
}
// Parse soundSample
const soundNode = findChild(root, "sound");
if (soundNode) {
const sampleNode = findChild(soundNode, "sample");
if (sampleNode) {
const sample: LogicResult["soundSample"] = {
id: parseInt(sampleNode.attrs.id || "0", 10),
};
if (sampleNode.attrs.noPitch === "1") sample.noPitch = true;
result.soundSample = sample;
}
}
// Parse customVars
const customVarsNode = findChild(root, "customVars");
if (customVarsNode) {
const variables: string[] = [];
for (const v of findChildren(customVarsNode, "variable")) {
if (v.attrs.name) variables.push(v.attrs.name);
}
if (variables.length > 0) result.customVars = { variables };
}
// Parse particleSystems
const particleNode = findChild(root, "particleSystems");
if (particleNode) {
const systems: Record<string, unknown>[] = [];
for (const ps of findChildren(particleNode, "particleSystem")) {
const system: Record<string, unknown> = {};
if (ps.attrs.size) system.size = parseInt(ps.attrs.size, 10);
if (ps.attrs.canvasId) system.canvasId = parseInt(ps.attrs.canvasId, 10);
if (ps.attrs.offsetY) system.offsetY = parseInt(ps.attrs.offsetY, 10);
if (ps.attrs.blend) system.blend = parseInt(ps.attrs.blend, 10);
if (ps.attrs.bgColor) system.bgColor = ps.attrs.bgColor;
const emitters: Record<string, unknown>[] = [];
for (const em of findChildren(ps, "emitter")) {
const emitter: Record<string, unknown> = {};
if (em.attrs.id) emitter.id = parseInt(em.attrs.id, 10);
if (em.attrs.name) emitter.name = em.attrs.name;
if (em.attrs.spriteId)
emitter.spriteId = parseInt(em.attrs.spriteId, 10);
if (em.attrs.maxNumParticles)
emitter.maxNumParticles = parseInt(em.attrs.maxNumParticles, 10);
if (em.attrs.particlesPerFrame)
emitter.particlesPerFrame = parseInt(em.attrs.particlesPerFrame, 10);
if (em.attrs.burstPulse)
emitter.burstPulse = parseInt(em.attrs.burstPulse, 10);
if (em.attrs.fuseTime)
emitter.fuseTime = parseInt(em.attrs.fuseTime, 10);
const simNode = findChild(em, "simulation");
if (simNode) {
const simulation: Record<string, unknown> = {};
if (simNode.attrs.force)
simulation.force = parseFloat(simNode.attrs.force);
if (simNode.attrs.direction)
simulation.direction = parseFloat(simNode.attrs.direction);
if (simNode.attrs.gravity)
simulation.gravity = parseFloat(simNode.attrs.gravity);
if (simNode.attrs.airFriction)
simulation.airFriction = parseFloat(simNode.attrs.airFriction);
if (simNode.attrs.shape) simulation.shape = simNode.attrs.shape;
if (simNode.attrs.energy)
simulation.energy = parseFloat(simNode.attrs.energy);
emitter.simulation = simulation;
}
const particles: Record<string, unknown>[] = [];
for (const p of findChildren(em, "particle")) {
const particle: Record<string, unknown> = {};
if (p.attrs.isEmitter === "1") particle.isEmitter = true;
if (p.attrs.lifeTime)
particle.lifeTime = parseInt(p.attrs.lifeTime, 10);
if (p.attrs.fade === "1") particle.fade = true;
const pFrames: string[] = [];
for (const pf of findChildren(p, "frame")) {
if (pf.attrs.name) pFrames.push(pf.attrs.name);
}
if (pFrames.length > 0) particle.frames = pFrames;
particles.push(particle);
}
if (particles.length > 0) emitter.particles = particles;
emitters.push(emitter);
}
if (emitters.length > 0) system.emitters = emitters;
systems.push(system);
}
if (systems.length > 0) result.particleSystems = systems;
}
// Parse planetSystems
const planetNode = findChild(root, "planetSystems");
if (planetNode) {
const systems: Record<string, unknown>[] = [];
for (const ps of planetNode.children) {
const system: Record<string, unknown> = { ...ps.attrs };
// Convert numeric attrs
for (const key of Object.keys(system)) {
const val = system[key] as string;
if (/^-?\d+(\.\d+)?$/.test(val)) system[key] = parseFloat(val);
}
systems.push(system);
}
if (systems.length > 0) result.planetSystems = systems;
}
return result;
}
export function processVisualization(xml: string): NitroVisualization[] {
const result: NitroVisualization[] = [];
const root = parseXml(xml);
if (!root) return result;
const graphics = findChild(root, "graphics") || root;
for (const viz of findChildren(graphics, "visualization")) {
const size = parseInt(viz.attrs.size || "64", 10);
// Skip size 32 (small catalog icons) – only keep size 1 (special) and 64 (standard)
if (size === 32) continue;
const layerCount = parseInt(viz.attrs.layerCount || "1", 10);
const angle = parseInt(viz.attrs.angle || "45", 10);
const entry: NitroVisualization = { layerCount, angle, size };
// Layers (before directions to match reference key order)
const layersNode = findChild(viz, "layers");
if (layersNode) {
const layers: Record<string, Record<string, unknown>> = {};
for (const l of findChildren(layersNode, "layer")) {
const id = l.attrs.id || "0";
const layerObj: Record<string, unknown> = {};
if (l.attrs.x) layerObj.x = parseInt(l.attrs.x, 10);
if (l.attrs.y) layerObj.y = parseInt(l.attrs.y, 10);
if (l.attrs.z) layerObj.z = parseInt(l.attrs.z, 10);
if (l.attrs.alpha) layerObj.alpha = parseInt(l.attrs.alpha, 10);
if (l.attrs.ink) layerObj.ink = l.attrs.ink;
if (l.attrs.tag) layerObj.tag = l.attrs.tag;
if (l.attrs.ignoreMouse)
layerObj.ignoreMouse = l.attrs.ignoreMouse === "1";
layers[id] = layerObj;
}
if (Object.keys(layers).length > 0) entry.layers = layers;
}
// Directions
const dirsNode = findChild(viz, "directions");
if (dirsNode) {
const dirs: Record<string, Record<string, unknown>> = {};
for (const d of findChildren(dirsNode, "direction")) {
const id = d.attrs.id || "0";
const layersInDir = findChildren(d, "layer");
if (layersInDir.length > 0) {
const layerData: Record<string, Record<string, unknown>> = {};
for (const l of layersInDir) {
const lid = l.attrs.id || "0";
const layerObj: Record<string, unknown> = {};
if (l.attrs.x) layerObj.x = parseInt(l.attrs.x, 10);
if (l.attrs.y) layerObj.y = parseInt(l.attrs.y, 10);
if (l.attrs.z) layerObj.z = parseFloat(l.attrs.z);
if (l.attrs.alpha) layerObj.alpha = parseInt(l.attrs.alpha, 10);
if (l.attrs.ink) layerObj.ink = l.attrs.ink;
if (l.attrs.tag) layerObj.tag = l.attrs.tag;
if (l.attrs.ignoreMouse)
layerObj.ignoreMouse = l.attrs.ignoreMouse === "1";
layerData[lid] = layerObj;
}
dirs[id] = { layers: layerData };
} else {
dirs[id] = {};
}
}
entry.directions = dirs;
}
// Colors
const colorsNode = findChild(viz, "colors");
if (colorsNode) {
const colors: Record<
string,
{ layers: Record<string, { color: number }> }
> = {};
for (const color of findChildren(colorsNode, "color")) {
const cid = color.attrs.id || "0";
const colorLayers: Record<string, { color: number }> = {};
for (const cl of findChildren(color, "colorLayer")) {
colorLayers[cl.attrs.id || "0"] = {
color: parseInt(cl.attrs.color || "0", 16),
};
}
colors[cid] = { layers: colorLayers };
}
if (Object.keys(colors).length > 0) entry.colors = colors;
}
// Animations
const animsNode = findChild(viz, "animations");
if (animsNode) {
const animations: Record<string, unknown> = {};
for (const anim of findChildren(animsNode, "animation")) {
const aid = anim.attrs.id || "0";
const animLayers: Record<string, unknown> = {};
for (const al of findChildren(anim, "animationLayer")) {
const alid = al.attrs.id || "0";
const frameSeqs: Record<string, unknown> = {};
for (const fs of findChildren(al, "frameSequence")) {
const fsid =
fs.attrs.id || Object.keys(frameSeqs).length.toString();
const frames: Record<string, Record<string, unknown>> = {};
let fi = 0;
for (const f of findChildren(fs, "frame")) {
const rawId = f.attrs.id || "0";
const frameObj: Record<string, unknown> = {
id: (rawId === "NaN" ? 0 : parseInt(rawId, 10)) || 0,
};
if (f.attrs.x) frameObj.x = parseInt(f.attrs.x, 10);
if (f.attrs.y) frameObj.y = parseInt(f.attrs.y, 10);
if (f.attrs.randomX)
frameObj.randomX = parseInt(f.attrs.randomX, 10);
if (f.attrs.randomY)
frameObj.randomY = parseInt(f.attrs.randomY, 10);
// Per-direction offsets (keyed by sequential index, matching Retrosprite format)
const offsetNodes = findChildren(f, "offset");
if (offsetNodes.length > 0) {
const offsets: Record<
string,
{ direction: number; x: number; y: number }
> = {};
for (let oi = 0; oi < offsetNodes.length; oi++) {
const o = offsetNodes[oi];
offsets[String(oi)] = {
direction: parseInt(o.attrs.direction || "0", 10),
x: parseInt(o.attrs.x || "0", 10),
y: parseInt(o.attrs.y || "0", 10),
};
}
frameObj.offsets = offsets;
}
frames[String(fi)] = frameObj;
fi++;
}
const seqObj: Record<string, unknown> = { frames };
if (fs.attrs.loopCount)
seqObj.loopCount = parseInt(fs.attrs.loopCount, 10);
if (fs.attrs.random) seqObj.random = parseInt(fs.attrs.random, 10);
frameSeqs[fsid] = seqObj;
}
const layerObj: Record<string, unknown> = {
loopCount: parseInt(al.attrs.loopCount || "0", 10),
frameSequences: frameSeqs,
};
if (al.attrs.frameRepeat)
layerObj.frameRepeat = parseInt(al.attrs.frameRepeat, 10);
if (al.attrs.random) layerObj.random = parseInt(al.attrs.random, 10);
animLayers[alid] = layerObj;
}
const animObj: Record<string, unknown> = { layers: animLayers };
if (anim.attrs.transitionTo)
animObj.transitionTo = parseInt(anim.attrs.transitionTo, 10);
if (anim.attrs.transitionFrom)
animObj.transitionFrom = parseInt(anim.attrs.transitionFrom, 10);
if (anim.attrs.immediateChangeFrom)
animObj.immediateChangeFrom = anim.attrs.immediateChangeFrom;
if (anim.attrs.randomStart === "1") animObj.randomStart = true;
animations[aid] = animObj;
}
if (Object.keys(animations).length > 0) entry.animations = animations;
}
// Postures
const posturesNode = findChild(viz, "postures");
if (posturesNode) {
const posturesList: { id: string; animationId: number }[] = [];
for (const p of findChildren(posturesNode, "posture")) {
posturesList.push({
id: p.attrs.id || "",
animationId: parseInt(p.attrs.animationId || "0", 10),
});
}
const posturesObj: NitroVisualization["postures"] = {};
if (posturesNode.attrs.defaultPosture)
posturesObj.defaultPosture = posturesNode.attrs.defaultPosture;
if (posturesList.length > 0) posturesObj.postures = posturesList;
entry.postures = posturesObj;
}
// Gestures
const gesturesNode = findChild(viz, "gestures");
if (gesturesNode) {
const gesturesList: { id: string; animationId: number }[] = [];
for (const g of findChildren(gesturesNode, "gesture")) {
gesturesList.push({
id: g.attrs.id || "",
animationId: parseInt(g.attrs.animationId || "0", 10),
});
}
if (gesturesList.length > 0) entry.gestures = gesturesList;
}
result.push(entry);
}
return result;
}
// ── Metadata Helpers ────────────────────────────────────────────────
export function extractPartColors(visualizations: NitroVisualization[]): {
color: string[];
} {
const viz64 = visualizations.find((v) => v.size === 64);
if (!viz64?.colors) return { color: [] };
return { color: Object.keys(viz64.colors) };
}
export function computeDefaultDir(directions: number[]): number {
if (directions.includes(2)) return 2;
if (directions.includes(0)) return 0;
return directions[0] || 0;
}