/** * 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>; directions?: Record>; colors?: Record; animations?: Record; postures?: { defaultPosture?: string; postures?: { id: string; animationId: number }[]; }; gestures?: { id: string; animationId: number }[]; } // ── Minimal XML Parser ───────────────────────────────────────────── export interface XmlNode { tag: string; attrs: Record; 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(//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(" 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 = {}; 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; palettes: Record; } { const offsets = new Map(); const palettes: Record = {}; 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[]; planetSystems?: Record[]; } 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[] = []; for (const ps of findChildren(particleNode, "particleSystem")) { const system: Record = {}; 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[] = []; for (const em of findChildren(ps, "emitter")) { const emitter: Record = {}; 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 = {}; 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[] = []; for (const p of findChildren(em, "particle")) { const particle: Record = {}; 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[] = []; for (const ps of planetNode.children) { const system: Record = { ...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> = {}; for (const l of findChildren(layersNode, "layer")) { const id = l.attrs.id || "0"; const layerObj: Record = {}; 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> = {}; 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> = {}; for (const l of layersInDir) { const lid = l.attrs.id || "0"; const layerObj: Record = {}; 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 } > = {}; for (const color of findChildren(colorsNode, "color")) { const cid = color.attrs.id || "0"; const colorLayers: Record = {}; 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 = {}; for (const anim of findChildren(animsNode, "animation")) { const aid = anim.attrs.id || "0"; const animLayers: Record = {}; for (const al of findChildren(anim, "animationLayer")) { const alid = al.attrs.id || "0"; const frameSeqs: Record = {}; for (const fs of findChildren(al, "frameSequence")) { const fsid = fs.attrs.id || Object.keys(frameSeqs).length.toString(); const frames: Record> = {}; let fi = 0; for (const f of findChildren(fs, "frame")) { const rawId = f.attrs.id || "0"; const frameObj: Record = { 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 = { 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 = { 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 = { 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; }