/** * 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 (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()! 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 }> = {} 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 = {} 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 }