This commit is contained in:
1 parent
e85e4d74ea
commit
2e4ed76121
378 files changed
+22404
-20686
No files matched your search
@@ -1,188 +1,188 @@
|
||||
import { inflateSync } from 'node:zlib'
|
||||
import * as jpeg from 'jpeg-js'
|
||||
import { inflateSync } from "node:zlib";
|
||||
import * as jpeg from "jpeg-js";
|
||||
|
||||
export interface SpriteImage {
|
||||
id: number
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
pixels: Buffer // RGBA
|
||||
id: number;
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer; // RGBA
|
||||
}
|
||||
|
||||
// ── DefineBitsLossless2 Parser (with alpha) ──────────────────────
|
||||
|
||||
export function parseBitsLossless2(data: Buffer): {
|
||||
id: number
|
||||
width: number
|
||||
height: number
|
||||
pixels: Buffer
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
} {
|
||||
let offset = 0
|
||||
const id = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const format = data.readUInt8(offset)
|
||||
offset += 1
|
||||
const width = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const height = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
let colorTableSize = 0
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1
|
||||
offset += 1
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
const compressed = data.subarray(offset)
|
||||
let decompressed: Buffer
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed)
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) }
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
|
||||
const pixels = Buffer.alloc(width * height * 4)
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
|
||||
if (format === 5) {
|
||||
let srcOff = 0
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break
|
||||
const a = decompressed[srcOff]
|
||||
let r = decompressed[srcOff + 1]
|
||||
let g = decompressed[srcOff + 2]
|
||||
let b = decompressed[srcOff + 3]
|
||||
srcOff += 4
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
const a = decompressed[srcOff];
|
||||
let r = decompressed[srcOff + 1];
|
||||
let g = decompressed[srcOff + 2];
|
||||
let b = decompressed[srcOff + 3];
|
||||
srcOff += 4;
|
||||
|
||||
// Un-premultiply alpha
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a))
|
||||
g = Math.min(255, Math.round((g * 255) / a))
|
||||
b = Math.min(255, Math.round((b * 255) / a))
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
|
||||
const dstOff = (y * width + x) * 4
|
||||
pixels[dstOff] = r
|
||||
pixels[dstOff + 1] = g
|
||||
pixels[dstOff + 2] = b
|
||||
pixels[dstOff + 3] = a
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = a;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = []
|
||||
let palOff = 0
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const a = decompressed[palOff]
|
||||
let r = decompressed[palOff + 1]
|
||||
let g = decompressed[palOff + 2]
|
||||
let b = decompressed[palOff + 3]
|
||||
const a = decompressed[palOff];
|
||||
let r = decompressed[palOff + 1];
|
||||
let g = decompressed[palOff + 2];
|
||||
let b = decompressed[palOff + 3];
|
||||
// Un-premultiply alpha for palette entries (same as format 5)
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a))
|
||||
g = Math.min(255, Math.round((g * 255) / a))
|
||||
b = Math.min(255, Math.round((b * 255) / a))
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
palette.push([r, g, b, a])
|
||||
palOff += 4
|
||||
palette.push([r, g, b, a]);
|
||||
palOff += 4;
|
||||
}
|
||||
|
||||
const rowPadded = Math.ceil(width / 4) * 4
|
||||
let srcOff = colorTableSize * 4
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 4;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x]
|
||||
const color = palette[idx] || [0, 0, 0, 0]
|
||||
const dstOff = (y * width + x) * 4
|
||||
pixels[dstOff] = color[0]
|
||||
pixels[dstOff + 1] = color[1]
|
||||
pixels[dstOff + 2] = color[2]
|
||||
pixels[dstOff + 3] = color[3]
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 0];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width, height, pixels }
|
||||
return { id, width, height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsLossless Parser (without alpha) ──────────────────────
|
||||
|
||||
export function parseBitsLossless(data: Buffer): {
|
||||
id: number
|
||||
width: number
|
||||
height: number
|
||||
pixels: Buffer
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
} {
|
||||
let offset = 0
|
||||
const id = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const format = data.readUInt8(offset)
|
||||
offset += 1
|
||||
const width = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const height = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
let colorTableSize = 0
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1
|
||||
offset += 1
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
const compressed = data.subarray(offset)
|
||||
let decompressed: Buffer
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed)
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) }
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
|
||||
const pixels = Buffer.alloc(width * height * 4)
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
|
||||
if (format === 5) {
|
||||
let srcOff = 0
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break
|
||||
srcOff++ // skip padding byte
|
||||
const r = decompressed[srcOff++]
|
||||
const g = decompressed[srcOff++]
|
||||
const b = decompressed[srcOff++]
|
||||
const dstOff = (y * width + x) * 4
|
||||
pixels[dstOff] = r
|
||||
pixels[dstOff + 1] = g
|
||||
pixels[dstOff + 2] = b
|
||||
pixels[dstOff + 3] = 255
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
srcOff++; // skip padding byte
|
||||
const r = decompressed[srcOff++];
|
||||
const g = decompressed[srcOff++];
|
||||
const b = decompressed[srcOff++];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = 255;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = []
|
||||
let palOff = 0
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const r = decompressed[palOff]
|
||||
const g = decompressed[palOff + 1]
|
||||
const b = decompressed[palOff + 2]
|
||||
palette.push([r, g, b, 255])
|
||||
palOff += 3
|
||||
const r = decompressed[palOff];
|
||||
const g = decompressed[palOff + 1];
|
||||
const b = decompressed[palOff + 2];
|
||||
palette.push([r, g, b, 255]);
|
||||
palOff += 3;
|
||||
}
|
||||
|
||||
const rowPadded = Math.ceil(width / 4) * 4
|
||||
let srcOff = colorTableSize * 3
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 3;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x]
|
||||
const color = palette[idx] || [0, 0, 0, 255]
|
||||
const dstOff = (y * width + x) * 4
|
||||
pixels[dstOff] = color[0]
|
||||
pixels[dstOff + 1] = color[1]
|
||||
pixels[dstOff + 2] = color[2]
|
||||
pixels[dstOff + 3] = color[3]
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 255];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width, height, pixels }
|
||||
return { id, width, height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsJpeg2 Parser (JPEG without alpha) ───────────────────
|
||||
@@ -190,10 +190,10 @@ export function parseBitsLossless(data: Buffer): {
|
||||
export function parseBitsJpeg2(
|
||||
data: Buffer,
|
||||
): { id: number; width: number; height: number; pixels: Buffer } | null {
|
||||
if (data.length < 6) return null
|
||||
if (data.length < 6) return null;
|
||||
|
||||
const id = data.readUInt16LE(0)
|
||||
let jpegData = data.subarray(2)
|
||||
const id = data.readUInt16LE(0);
|
||||
let jpegData = data.subarray(2);
|
||||
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
@@ -203,14 +203,14 @@ export function parseBitsJpeg2(
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4)
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
|
||||
const dims = parseJpegDimensions(jpegData)
|
||||
if (!dims) return null
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height)
|
||||
return { id, width: dims.width, height: dims.height, pixels }
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsJpeg3 Parser (JPEG with alpha channel) ──────────────
|
||||
@@ -218,11 +218,11 @@ export function parseBitsJpeg2(
|
||||
export function parseBitsJpeg3(
|
||||
data: Buffer,
|
||||
): { id: number; width: number; height: number; pixels: Buffer } | null {
|
||||
if (data.length < 6) return null
|
||||
if (data.length < 6) return null;
|
||||
|
||||
const id = data.readUInt16LE(0)
|
||||
const alphaDataOffset = data.readUInt32LE(2)
|
||||
let jpegData = data.subarray(6, 6 + alphaDataOffset)
|
||||
const id = data.readUInt16LE(0);
|
||||
const alphaDataOffset = data.readUInt32LE(2);
|
||||
let jpegData = data.subarray(6, 6 + alphaDataOffset);
|
||||
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
@@ -232,72 +232,72 @@ export function parseBitsJpeg3(
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4)
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
|
||||
const dims = parseJpegDimensions(jpegData)
|
||||
if (!dims) return null
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height)
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
|
||||
// Apply zlib-compressed alpha channel
|
||||
const compressedAlpha = data.subarray(6 + alphaDataOffset)
|
||||
const compressedAlpha = data.subarray(6 + alphaDataOffset);
|
||||
if (compressedAlpha.length > 0) {
|
||||
try {
|
||||
const alpha = inflateSync(compressedAlpha)
|
||||
const pixelCount = dims.width * dims.height
|
||||
const alpha = inflateSync(compressedAlpha);
|
||||
const pixelCount = dims.width * dims.height;
|
||||
for (let i = 0; i < pixelCount && i < alpha.length; i++) {
|
||||
pixels[i * 4 + 3] = alpha[i]
|
||||
pixels[i * 4 + 3] = alpha[i];
|
||||
}
|
||||
} catch {
|
||||
// Alpha decompression failed, keep fully opaque
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width: dims.width, height: dims.height, pixels }
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
}
|
||||
|
||||
// ── JPEG Dimension Parser (reads SOF0/SOF2 markers) ───────────────
|
||||
|
||||
export function parseJpegDimensions(jpeg: Buffer): { width: number; height: number } | null {
|
||||
let offset = 0
|
||||
let offset = 0;
|
||||
while (offset < jpeg.length - 1) {
|
||||
if (jpeg[offset] !== 0xff) {
|
||||
offset++
|
||||
continue
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const marker = jpeg[offset + 1]
|
||||
offset += 2
|
||||
const marker = jpeg[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
// SOF0 (Baseline), SOF1 (Extended), SOF2 (Progressive)
|
||||
if (marker >= 0xc0 && marker <= 0xc2) {
|
||||
if (offset + 7 > jpeg.length) return null
|
||||
if (offset + 7 > jpeg.length) return null;
|
||||
// Skip length (2 bytes) + precision (1 byte)
|
||||
const height = jpeg.readUInt16BE(offset + 3)
|
||||
const width = jpeg.readUInt16BE(offset + 5)
|
||||
return { width, height }
|
||||
const height = jpeg.readUInt16BE(offset + 3);
|
||||
const width = jpeg.readUInt16BE(offset + 5);
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// Skip non-SOF markers with length field
|
||||
if (marker === 0xd8 || marker === 0xd9) continue // SOI / EOI
|
||||
if (marker >= 0xd0 && marker <= 0xd7) continue // RST markers
|
||||
if (offset + 2 > jpeg.length) break
|
||||
const segLen = jpeg.readUInt16BE(offset)
|
||||
offset += segLen
|
||||
if (marker === 0xd8 || marker === 0xd9) continue; // SOI / EOI
|
||||
if (marker >= 0xd0 && marker <= 0xd7) continue; // RST markers
|
||||
if (offset + 2 > jpeg.length) break;
|
||||
const segLen = jpeg.readUInt16BE(offset);
|
||||
offset += segLen;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── JPEG to RGBA decoder using jpeg-js ──────────────────────────────
|
||||
|
||||
export function decodeJpegToRGBA(jpegData: Buffer, width: number, height: number): Buffer {
|
||||
try {
|
||||
const decoded = jpeg.decode(jpegData, { useTArray: true, formatAsRGBA: true })
|
||||
const decoded = jpeg.decode(jpegData, { useTArray: true, formatAsRGBA: true });
|
||||
// jpeg-js returns RGBA data directly
|
||||
return Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength)
|
||||
return Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength);
|
||||
} catch {
|
||||
// Fallback: transparent pixels if JPEG decode fails
|
||||
return Buffer.alloc(width * height * 4)
|
||||
return Buffer.alloc(width * height * 4);
|
||||
}
|
||||
}
|
||||
+185
-185
@@ -5,16 +5,16 @@
|
||||
* then bundles everything into the .nitro format expected
|
||||
* by the Nitro HTML5 client.
|
||||
*/
|
||||
import { gunzipSync, inflateSync } from 'node:zlib'
|
||||
import type { BatchConversionItem, BatchConversionResult, ConversionResult } from '@/types/furni'
|
||||
import { gunzipSync, inflateSync } from "node:zlib";
|
||||
import type { BatchConversionItem, BatchConversionResult, ConversionResult } from "@/types/furni";
|
||||
import {
|
||||
parseBitsJpeg2,
|
||||
parseBitsJpeg3,
|
||||
parseBitsLossless,
|
||||
parseBitsLossless2,
|
||||
type SpriteImage,
|
||||
} from './image-decoder'
|
||||
import { buildSpritesheet, createNitroBundle, encodePng } from './nitro-builder'
|
||||
} from "./image-decoder";
|
||||
import { buildSpritesheet, createNitroBundle, encodePng } from "./nitro-builder";
|
||||
import {
|
||||
decompressSwf,
|
||||
parseBinaryData,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
TAG_DEFINE_BITS_LOSSLESS,
|
||||
TAG_DEFINE_BITS_LOSSLESS2,
|
||||
TAG_SYMBOL_CLASS,
|
||||
} from './swf-parser'
|
||||
} from "./swf-parser";
|
||||
import {
|
||||
computeDefaultDir,
|
||||
extractPartColors,
|
||||
@@ -39,151 +39,151 @@ import {
|
||||
processLogic,
|
||||
processManifest,
|
||||
processVisualization,
|
||||
} from './xml-processor'
|
||||
} from "./xml-processor";
|
||||
|
||||
export type {
|
||||
BatchConversionItem,
|
||||
BatchConversionResult,
|
||||
ConversionResult,
|
||||
FurniMetadata,
|
||||
} from '@/types/furni'
|
||||
} from "@/types/furni";
|
||||
// Re-export public API from submodules
|
||||
export { createNitroBundle, parseNitroBundle } from './nitro-builder'
|
||||
export { computeDefaultDir, extractPartColors } from './xml-processor'
|
||||
export { createNitroBundle, parseNitroBundle } from "./nitro-builder";
|
||||
export { computeDefaultDir, extractPartColors } from "./xml-processor";
|
||||
|
||||
// ── Main Conversion ────────────────────────────────────────────────
|
||||
|
||||
export function convertSwfToNitro(swfBuffer: Buffer, classname: string): ConversionResult {
|
||||
const warnings: string[] = []
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 1. Decompress SWF
|
||||
let decompressed: Buffer
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = decompressSwf(swfBuffer)
|
||||
decompressed = decompressSwf(swfBuffer);
|
||||
} catch (err) {
|
||||
throw new Error(`SWF decompression failed for ${classname}: ${(err as Error).message}`)
|
||||
throw new Error(`SWF decompression failed for ${classname}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// 2. Parse tags
|
||||
const tags = parseSwfTags(decompressed)
|
||||
const tags = parseSwfTags(decompressed);
|
||||
if (tags.length === 0) {
|
||||
throw new Error(`No tags found in SWF for ${classname}`)
|
||||
throw new Error(`No tags found in SWF for ${classname}`);
|
||||
}
|
||||
|
||||
// 3. Parse SymbolClass to map IDs to names
|
||||
let symbolResult: SymbolClassResult = { classNames: new Map(), symbols: new Map() }
|
||||
let symbolResult: SymbolClassResult = { classNames: new Map(), symbols: new Map() };
|
||||
for (const tag of tags) {
|
||||
if (tag.type === TAG_SYMBOL_CLASS) {
|
||||
symbolResult = parseSymbolClass(tag.data)
|
||||
break
|
||||
symbolResult = parseSymbolClass(tag.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const { classNames: symbolMap, symbols: allSymbols } = symbolResult
|
||||
const { classNames: symbolMap, symbols: allSymbols } = symbolResult;
|
||||
|
||||
// 4. Extract binary data (XML configs) and images
|
||||
const binaryData = new Map<string, Buffer>()
|
||||
const rawImages = new Map<number, { width: number; height: number; pixels: Buffer }>()
|
||||
const binaryData = new Map<string, Buffer>();
|
||||
const rawImages = new Map<number, { width: number; height: number; pixels: Buffer }>();
|
||||
|
||||
for (const tag of tags) {
|
||||
switch (tag.type) {
|
||||
case TAG_DEFINE_BINARY_DATA: {
|
||||
const { id, content } = parseBinaryData(tag.data)
|
||||
const name = symbolMap.get(id) || `binary_${id}`
|
||||
binaryData.set(name, content)
|
||||
break
|
||||
const { id, content } = parseBinaryData(tag.data);
|
||||
const name = symbolMap.get(id) || `binary_${id}`;
|
||||
binaryData.set(name, content);
|
||||
break;
|
||||
}
|
||||
case TAG_DEFINE_BITS_LOSSLESS2: {
|
||||
const img = parseBitsLossless2(tag.data)
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels })
|
||||
break
|
||||
const img = parseBitsLossless2(tag.data);
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
|
||||
break;
|
||||
}
|
||||
case TAG_DEFINE_BITS_LOSSLESS: {
|
||||
const img = parseBitsLossless(tag.data)
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels })
|
||||
break
|
||||
const img = parseBitsLossless(tag.data);
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
|
||||
break;
|
||||
}
|
||||
case TAG_DEFINE_BITS_JPEG2: {
|
||||
const img = parseBitsJpeg2(tag.data)
|
||||
const img = parseBitsJpeg2(tag.data);
|
||||
if (img && !rawImages.has(img.id)) {
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels })
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
|
||||
}
|
||||
break
|
||||
break;
|
||||
}
|
||||
case TAG_DEFINE_BITS_JPEG3: {
|
||||
const img = parseBitsJpeg3(tag.data)
|
||||
const img = parseBitsJpeg3(tag.data);
|
||||
if (img && !rawImages.has(img.id)) {
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels })
|
||||
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
|
||||
}
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Strip the SymbolClass package prefix
|
||||
const prefix = `${classname}_`
|
||||
const prefix = `${classname}_`;
|
||||
|
||||
function stripPrefix(name: string): string {
|
||||
if (name.startsWith(prefix)) return name.substring(prefix.length)
|
||||
if (name.includes('.')) return name.split('.').pop() || name
|
||||
return name
|
||||
if (name.startsWith(prefix)) return name.substring(prefix.length);
|
||||
if (name.includes(".")) return name.split(".").pop() || name;
|
||||
return name;
|
||||
}
|
||||
|
||||
// 6. Parse XML data from binary tags
|
||||
let assetsXml = ''
|
||||
let indexXml = ''
|
||||
let logicXml = ''
|
||||
let visualizationXml = ''
|
||||
let manifestXml = ''
|
||||
let assetsXml = "";
|
||||
let indexXml = "";
|
||||
let logicXml = "";
|
||||
let visualizationXml = "";
|
||||
let manifestXml = "";
|
||||
|
||||
for (const [rawName, content] of binaryData) {
|
||||
let xmlStr = content.toString('utf-8').trim()
|
||||
xmlStr = xmlStr.replace(/encoding="[Ii][Ss][Oo]-8859-1"/g, 'encoding="UTF-8"')
|
||||
const name = stripPrefix(rawName).toLowerCase()
|
||||
if (name.includes('manifest') || name.endsWith('_manifest')) {
|
||||
manifestXml = xmlStr
|
||||
} else if (name.includes('assets') || name.endsWith('_assets')) {
|
||||
assetsXml = xmlStr
|
||||
} else if (name === 'index' || name.endsWith('_index')) {
|
||||
indexXml = xmlStr
|
||||
} else if (name.includes('logic') || name.endsWith('_logic')) {
|
||||
logicXml = xmlStr
|
||||
} else if (name.includes('visualization') || name.endsWith('_visualization')) {
|
||||
visualizationXml = xmlStr
|
||||
let xmlStr = content.toString("utf-8").trim();
|
||||
xmlStr = xmlStr.replace(/encoding="[Ii][Ss][Oo]-8859-1"/g, 'encoding="UTF-8"');
|
||||
const name = stripPrefix(rawName).toLowerCase();
|
||||
if (name.includes("manifest") || name.endsWith("_manifest")) {
|
||||
manifestXml = xmlStr;
|
||||
} else if (name.includes("assets") || name.endsWith("_assets")) {
|
||||
assetsXml = xmlStr;
|
||||
} else if (name === "index" || name.endsWith("_index")) {
|
||||
indexXml = xmlStr;
|
||||
} else if (name.includes("logic") || name.endsWith("_logic")) {
|
||||
logicXml = xmlStr;
|
||||
} else if (name.includes("visualization") || name.endsWith("_visualization")) {
|
||||
visualizationXml = xmlStr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!assetsXml) warnings.push('No assets XML found in SWF')
|
||||
if (!logicXml) warnings.push('No logic XML found in SWF')
|
||||
if (!visualizationXml) warnings.push('No visualization XML found in SWF')
|
||||
if (!assetsXml) warnings.push("No assets XML found in SWF");
|
||||
if (!logicXml) warnings.push("No logic XML found in SWF");
|
||||
if (!visualizationXml) warnings.push("No visualization XML found in SWF");
|
||||
|
||||
// Build set of needed sprites from assets.xml
|
||||
const neededSprites = new Set<string>()
|
||||
const neededSprites = new Set<string>();
|
||||
if (assetsXml) {
|
||||
const assetsRoot = parseXml(assetsXml)
|
||||
const assetsRoot = parseXml(assetsXml);
|
||||
if (assetsRoot) {
|
||||
const assetsNode =
|
||||
assetsRoot.tag === 'assets' ? assetsRoot : findChild(assetsRoot, 'assets') || assetsRoot
|
||||
for (const asset of findChildren(assetsNode, 'asset')) {
|
||||
const assetName = asset.attrs.name || ''
|
||||
if (!assetName || assetName.startsWith('sh_') || assetName.includes('_32_')) continue
|
||||
assetsRoot.tag === "assets" ? assetsRoot : findChild(assetsRoot, "assets") || assetsRoot;
|
||||
for (const asset of findChildren(assetsNode, "asset")) {
|
||||
const assetName = asset.attrs.name || "";
|
||||
if (!assetName || assetName.startsWith("sh_") || assetName.includes("_32_")) continue;
|
||||
if (!asset.attrs.source) {
|
||||
neededSprites.add(assetName)
|
||||
neededSprites.add(assetName);
|
||||
} else {
|
||||
neededSprites.add(asset.attrs.source)
|
||||
neededSprites.add(asset.attrs.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map images to named sprites
|
||||
const namedImages: SpriteImage[] = []
|
||||
const namedImages: SpriteImage[] = [];
|
||||
for (const [id, imgData] of rawImages) {
|
||||
const rawName = symbolMap.get(id)
|
||||
if (!rawName) continue
|
||||
const name = stripPrefix(rawName)
|
||||
const rawName = symbolMap.get(id);
|
||||
if (!rawName) continue;
|
||||
const name = stripPrefix(rawName);
|
||||
|
||||
if (name.startsWith('sh_') || name.includes('_32_')) continue
|
||||
if (neededSprites.size > 0 && !neededSprites.has(name)) continue
|
||||
if (name.startsWith("sh_") || name.includes("_32_")) continue;
|
||||
if (neededSprites.size > 0 && !neededSprites.has(name)) continue;
|
||||
|
||||
namedImages.push({
|
||||
id,
|
||||
@@ -191,29 +191,29 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
width: imgData.width,
|
||||
height: imgData.height,
|
||||
pixels: imgData.pixels,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
namedImages.sort((a, b) => a.name.localeCompare(b.name))
|
||||
namedImages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// 7. Build spritesheet using bin-packing
|
||||
const { png, frames, width: sheetWidth, height: sheetHeight } = buildSpritesheet(namedImages)
|
||||
const { png, frames, width: sheetWidth, height: sheetHeight } = buildSpritesheet(namedImages);
|
||||
|
||||
// 8. Process XML into Nitro JSON structures
|
||||
const { palettes } = processAssetsXml(assetsXml)
|
||||
const manifest = processManifest(manifestXml)
|
||||
const indexData = processIndex(indexXml)
|
||||
const logic = processLogic(logicXml)
|
||||
const visualizations = processVisualization(visualizationXml)
|
||||
const { palettes } = processAssetsXml(assetsXml);
|
||||
const manifest = processManifest(manifestXml);
|
||||
const indexData = processIndex(indexXml);
|
||||
const logic = processLogic(logicXml);
|
||||
const visualizations = processVisualization(visualizationXml);
|
||||
|
||||
// Build IMAGE_SOURCES map
|
||||
const imageSources = new Map<string, string>()
|
||||
const imageSources = new Map<string, string>();
|
||||
for (const [symbolName, charId] of allSymbols) {
|
||||
if (!rawImages.has(charId)) continue
|
||||
const canonicalName = symbolMap.get(charId)
|
||||
if (!canonicalName) continue
|
||||
if (!rawImages.has(charId)) continue;
|
||||
const canonicalName = symbolMap.get(charId);
|
||||
if (!canonicalName) continue;
|
||||
if (symbolName !== canonicalName) {
|
||||
imageSources.set(stripPrefix(symbolName), stripPrefix(canonicalName))
|
||||
imageSources.set(stripPrefix(symbolName), stripPrefix(canonicalName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,82 +221,82 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
const assets: Record<
|
||||
string,
|
||||
{
|
||||
x: number
|
||||
y: number
|
||||
source?: string
|
||||
flipH?: boolean
|
||||
flipV?: boolean
|
||||
usesPalette?: boolean
|
||||
x: number;
|
||||
y: number;
|
||||
source?: string;
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
usesPalette?: boolean;
|
||||
}
|
||||
> = {}
|
||||
> = {};
|
||||
|
||||
for (const ma of manifest.assets) {
|
||||
assets[ma.name] = { x: ma.x, y: ma.y }
|
||||
assets[ma.name] = { x: ma.x, y: ma.y };
|
||||
}
|
||||
|
||||
if (assetsXml) {
|
||||
const root = parseXml(assetsXml)
|
||||
const root = parseXml(assetsXml);
|
||||
if (root) {
|
||||
const assetsNode = root.tag === 'assets' ? root : findChild(root, 'assets') || root
|
||||
for (const asset of findChildren(assetsNode, 'asset')) {
|
||||
const assetName = asset.attrs.name || ''
|
||||
if (!assetName) continue
|
||||
if (assetName.startsWith('sh_') || assetName.includes('_32_')) continue
|
||||
const assetsNode = root.tag === "assets" ? root : findChild(root, "assets") || root;
|
||||
for (const asset of findChildren(assetsNode, "asset")) {
|
||||
const assetName = asset.attrs.name || "";
|
||||
if (!assetName) continue;
|
||||
if (assetName.startsWith("sh_") || assetName.includes("_32_")) continue;
|
||||
|
||||
const x = parseInt(asset.attrs.x || '0', 10)
|
||||
const y = parseInt(asset.attrs.y || '0', 10)
|
||||
const x = parseInt(asset.attrs.x || "0", 10);
|
||||
const y = parseInt(asset.attrs.y || "0", 10);
|
||||
|
||||
let source = asset.attrs.source || ''
|
||||
let source = asset.attrs.source || "";
|
||||
if (source && imageSources.has(source)) {
|
||||
source = imageSources.get(source)!
|
||||
source = imageSources.get(source)!;
|
||||
}
|
||||
if (imageSources.has(assetName)) {
|
||||
source = imageSources.get(assetName)!
|
||||
source = imageSources.get(assetName)!;
|
||||
}
|
||||
|
||||
const entry: (typeof assets)[string] = source
|
||||
? ({ source, x, y } as (typeof assets)[string])
|
||||
: { x, y }
|
||||
if (asset.attrs.flipH === '1') {
|
||||
entry.flipH = true
|
||||
: { x, y };
|
||||
if (asset.attrs.flipH === "1") {
|
||||
entry.flipH = true;
|
||||
}
|
||||
if (asset.attrs.flipV === '1') {
|
||||
entry.flipV = true
|
||||
if (asset.attrs.flipV === "1") {
|
||||
entry.flipV = true;
|
||||
}
|
||||
if (asset.attrs.usesPalette === '1') {
|
||||
entry.usesPalette = true
|
||||
if (asset.attrs.usesPalette === "1") {
|
||||
entry.usesPalette = true;
|
||||
}
|
||||
|
||||
assets[assetName] = entry
|
||||
assets[assetName] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(assets).length === 0) {
|
||||
for (const img of namedImages) {
|
||||
assets[img.name] = { x: 0, y: 0 }
|
||||
assets[img.name] = { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
const validSpriteNames = new Set(namedImages.map((img) => img.name))
|
||||
const validSpriteNames = new Set(namedImages.map((img) => img.name));
|
||||
|
||||
for (const key of Object.keys(assets)) {
|
||||
if (!validSpriteNames.has(key) && !assets[key].source) {
|
||||
delete assets[key]
|
||||
delete assets[key];
|
||||
}
|
||||
}
|
||||
|
||||
const sortedAssets: typeof assets = {}
|
||||
const sortedAssets: typeof assets = {};
|
||||
for (const key of Object.keys(assets).sort()) {
|
||||
sortedAssets[key] = assets[key]
|
||||
sortedAssets[key] = assets[key];
|
||||
}
|
||||
|
||||
// Build spritesheet frames
|
||||
const spritesheetFrames: Record<string, unknown> = {}
|
||||
const spritesheetFrames: Record<string, unknown> = {};
|
||||
for (const img of namedImages) {
|
||||
const frameInfo = frames.get(img.name)
|
||||
if (!frameInfo) continue
|
||||
const frameKey = `${classname}_${img.name}`
|
||||
const frameInfo = frames.get(img.name);
|
||||
if (!frameInfo) continue;
|
||||
const frameKey = `${classname}_${img.name}`;
|
||||
spritesheetFrames[frameKey] = {
|
||||
frame: { x: frameInfo.x, y: frameInfo.y, w: frameInfo.w, h: frameInfo.h },
|
||||
rotated: false,
|
||||
@@ -304,17 +304,17 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
spriteSourceSize: { x: 0, y: 0, w: frameInfo.w, h: frameInfo.h },
|
||||
sourceSize: { w: frameInfo.w, h: frameInfo.h },
|
||||
pivot: { x: 0.5, y: 0.5 },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Determine visualization/logic types
|
||||
let visualizationType = indexData?.visualizationType || 'furniture_static'
|
||||
let logicType = indexData?.logicType || 'furniture_basic'
|
||||
let visualizationType = indexData?.visualizationType || "furniture_static";
|
||||
let logicType = indexData?.logicType || "furniture_basic";
|
||||
|
||||
if (!indexData) {
|
||||
if (visualizations.some((v) => v.animations && Object.keys(v.animations).length > 0)) {
|
||||
visualizationType = 'furniture_animated'
|
||||
logicType = 'furniture_multistate'
|
||||
visualizationType = "furniture_animated";
|
||||
logicType = "furniture_multistate";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,14 +324,14 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
dimensions: logic.dimensions,
|
||||
directions: logic.directions,
|
||||
},
|
||||
}
|
||||
if (logic.maskType) logicObj.maskType = logic.maskType
|
||||
if (logic.action) logicObj.action = logic.action
|
||||
if (logic.credits) logicObj.credits = logic.credits
|
||||
if (logic.soundSample) logicObj.soundSample = logic.soundSample
|
||||
if (logic.customVars) logicObj.customVars = logic.customVars
|
||||
if (logic.particleSystems) logicObj.particleSystems = logic.particleSystems
|
||||
if (logic.planetSystems) logicObj.planetSystems = logic.planetSystems
|
||||
};
|
||||
if (logic.maskType) logicObj.maskType = logic.maskType;
|
||||
if (logic.action) logicObj.action = logic.action;
|
||||
if (logic.credits) logicObj.credits = logic.credits;
|
||||
if (logic.soundSample) logicObj.soundSample = logic.soundSample;
|
||||
if (logic.customVars) logicObj.customVars = logic.customVars;
|
||||
if (logic.particleSystems) logicObj.particleSystems = logic.particleSystems;
|
||||
if (logic.planetSystems) logicObj.planetSystems = logic.planetSystems;
|
||||
|
||||
const nitroJson: Record<string, unknown> = {
|
||||
name: classname,
|
||||
@@ -340,24 +340,24 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
spritesheet: {
|
||||
meta: {
|
||||
image: `${classname}.png`,
|
||||
format: 'RGBA8888',
|
||||
format: "RGBA8888",
|
||||
size: { w: sheetWidth, h: sheetHeight },
|
||||
scale: 1,
|
||||
},
|
||||
frames: spritesheetFrames,
|
||||
},
|
||||
assets: sortedAssets,
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(palettes).length > 0) {
|
||||
nitroJson.palettes = palettes
|
||||
nitroJson.palettes = palettes;
|
||||
}
|
||||
|
||||
nitroJson.visualizations = visualizations
|
||||
nitroJson.logic = logicObj
|
||||
nitroJson.visualizations = visualizations;
|
||||
nitroJson.logic = logicObj;
|
||||
|
||||
// 10. Create .nitro bundle
|
||||
const bundle = createNitroBundle(nitroJson, png, classname)
|
||||
const bundle = createNitroBundle(nitroJson, png, classname);
|
||||
|
||||
return {
|
||||
bundle,
|
||||
@@ -376,74 +376,74 @@ export function convertSwfToNitro(swfBuffer: Buffer, classname: string): Convers
|
||||
defaultdir: computeDefaultDir(logic.directions),
|
||||
partcolors: extractPartColors(visualizations),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Icon Extraction from SWF ────────────────────────────────────────
|
||||
|
||||
export function extractIconFromSwf(swfBuffer: Buffer, classname: string): Buffer | null {
|
||||
try {
|
||||
const decompressed = decompressSwf(swfBuffer)
|
||||
const tags = parseSwfTags(decompressed)
|
||||
const decompressed = decompressSwf(swfBuffer);
|
||||
const tags = parseSwfTags(decompressed);
|
||||
|
||||
let iconSymbolMap = new Map<number, string>()
|
||||
let iconSymbolMap = new Map<number, string>();
|
||||
for (const tag of tags) {
|
||||
if (tag.type === TAG_SYMBOL_CLASS) {
|
||||
const result = parseSymbolClass(tag.data)
|
||||
iconSymbolMap = result.classNames
|
||||
break
|
||||
const result = parseSymbolClass(tag.data);
|
||||
iconSymbolMap = result.classNames;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = `${classname}_`
|
||||
const candidates: { name: string; width: number; height: number; pixels: Buffer }[] = []
|
||||
const prefix = `${classname}_`;
|
||||
const candidates: { name: string; width: number; height: number; pixels: Buffer }[] = [];
|
||||
|
||||
for (const tag of tags) {
|
||||
let img: { id: number; width: number; height: number; pixels: Buffer } | null = null
|
||||
if (tag.type === TAG_DEFINE_BITS_LOSSLESS2) img = parseBitsLossless2(tag.data)
|
||||
else if (tag.type === TAG_DEFINE_BITS_LOSSLESS) img = parseBitsLossless(tag.data)
|
||||
else if (tag.type === TAG_DEFINE_BITS_JPEG2) img = parseBitsJpeg2(tag.data)
|
||||
else if (tag.type === TAG_DEFINE_BITS_JPEG3) img = parseBitsJpeg3(tag.data)
|
||||
if (!img) continue
|
||||
let img: { id: number; width: number; height: number; pixels: Buffer } | null = null;
|
||||
if (tag.type === TAG_DEFINE_BITS_LOSSLESS2) img = parseBitsLossless2(tag.data);
|
||||
else if (tag.type === TAG_DEFINE_BITS_LOSSLESS) img = parseBitsLossless(tag.data);
|
||||
else if (tag.type === TAG_DEFINE_BITS_JPEG2) img = parseBitsJpeg2(tag.data);
|
||||
else if (tag.type === TAG_DEFINE_BITS_JPEG3) img = parseBitsJpeg3(tag.data);
|
||||
if (!img) continue;
|
||||
|
||||
const rawName = iconSymbolMap.get(img.id) || ''
|
||||
const name = rawName.startsWith(prefix) ? rawName.substring(prefix.length) : rawName
|
||||
if (!name || name.startsWith('sh_')) continue
|
||||
const rawName = iconSymbolMap.get(img.id) || "";
|
||||
const name = rawName.startsWith(prefix) ? rawName.substring(prefix.length) : rawName;
|
||||
if (!name || name.startsWith("sh_")) continue;
|
||||
|
||||
if (name.includes('icon') || /^1_[a-z]_\d+_\d+$/.test(name)) {
|
||||
candidates.push({ name, width: img.width, height: img.height, pixels: img.pixels })
|
||||
if (name.includes("icon") || /^1_[a-z]_\d+_\d+$/.test(name)) {
|
||||
candidates.push({ name, width: img.width, height: img.height, pixels: img.pixels });
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
for (const tag of tags) {
|
||||
let img: { id: number; width: number; height: number; pixels: Buffer } | null = null
|
||||
if (tag.type === TAG_DEFINE_BITS_LOSSLESS2) img = parseBitsLossless2(tag.data)
|
||||
else if (tag.type === TAG_DEFINE_BITS_LOSSLESS) img = parseBitsLossless(tag.data)
|
||||
if (!img || img.width < 2 || img.height < 2) continue
|
||||
let img: { id: number; width: number; height: number; pixels: Buffer } | null = null;
|
||||
if (tag.type === TAG_DEFINE_BITS_LOSSLESS2) img = parseBitsLossless2(tag.data);
|
||||
else if (tag.type === TAG_DEFINE_BITS_LOSSLESS) img = parseBitsLossless(tag.data);
|
||||
if (!img || img.width < 2 || img.height < 2) continue;
|
||||
|
||||
const rawName = iconSymbolMap.get(img.id) || ''
|
||||
const name = rawName.startsWith(prefix) ? rawName.substring(prefix.length) : rawName
|
||||
if (name.startsWith('sh_')) continue
|
||||
const rawName = iconSymbolMap.get(img.id) || "";
|
||||
const name = rawName.startsWith(prefix) ? rawName.substring(prefix.length) : rawName;
|
||||
if (name.startsWith("sh_")) continue;
|
||||
|
||||
candidates.push({ name, width: img.width, height: img.height, pixels: img.pixels })
|
||||
candidates.push({ name, width: img.width, height: img.height, pixels: img.pixels });
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) return null
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
candidates.sort((a, b) => a.width * a.height - b.width * b.height)
|
||||
const icon = candidates[0]
|
||||
return encodePng(icon.width, icon.height, icon.pixels)
|
||||
candidates.sort((a, b) => a.width * a.height - b.width * b.height);
|
||||
const icon = candidates[0];
|
||||
return encodePng(icon.width, icon.height, icon.pixels);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Backward-compatible wrapper ────────────────────────────────────
|
||||
|
||||
export function convertSwfToNitroBuffer(swfBuffer: Buffer, classname: string): Buffer {
|
||||
return convertSwfToNitro(swfBuffer, classname).bundle
|
||||
return convertSwfToNitro(swfBuffer, classname).bundle;
|
||||
}
|
||||
|
||||
// ── Batch Conversion ───────────────────────────────────────────────
|
||||
@@ -451,7 +451,7 @@ export function convertSwfToNitroBuffer(swfBuffer: Buffer, classname: string): B
|
||||
export function convertSwfToNitroBatch(items: BatchConversionItem[]): BatchConversionResult[] {
|
||||
return items.map((item) => {
|
||||
try {
|
||||
const result = convertSwfToNitro(item.swfBuffer, item.classname)
|
||||
const result = convertSwfToNitro(item.swfBuffer, item.classname);
|
||||
return {
|
||||
classname: item.classname,
|
||||
success: true,
|
||||
@@ -460,28 +460,28 @@ export function convertSwfToNitroBatch(items: BatchConversionItem[]): BatchConve
|
||||
directions: result.directions,
|
||||
spriteCount: result.spriteCount,
|
||||
warnings: result.warnings,
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
classname: item.classname,
|
||||
success: false,
|
||||
warnings: [],
|
||||
error: (err as Error).message,
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// ── Utility: Try to decompress nitro-style data (zlib or gzip) ─────
|
||||
|
||||
export function nitroDecompress(data: Buffer): Buffer {
|
||||
try {
|
||||
return inflateSync(data)
|
||||
return inflateSync(data);
|
||||
} catch {
|
||||
try {
|
||||
return gunzipSync(data)
|
||||
return gunzipSync(data);
|
||||
} catch {
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,91 @@
|
||||
import { deflateSync, inflateSync } from 'node:zlib'
|
||||
import type { SpriteImage } from './image-decoder'
|
||||
import { deflateSync, inflateSync } from "node:zlib";
|
||||
import type { SpriteImage } from "./image-decoder";
|
||||
|
||||
// ── Minimal PNG Encoder ────────────────────────────────────────────
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256)
|
||||
const table = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
||||
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
}
|
||||
table[n] = c
|
||||
table[n] = c;
|
||||
}
|
||||
return table
|
||||
})()
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(data: Buffer): number {
|
||||
let crc = 0xffffffff
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)
|
||||
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function pngChunk(type: string, data: Buffer): Buffer {
|
||||
const buf = Buffer.alloc(4 + 4 + data.length + 4)
|
||||
buf.writeUInt32BE(data.length, 0)
|
||||
buf.write(type, 4, 4, 'ascii')
|
||||
data.copy(buf, 8)
|
||||
const crc = crc32(buf.subarray(4, 8 + data.length))
|
||||
buf.writeUInt32BE(crc >>> 0, 8 + data.length)
|
||||
return buf
|
||||
const buf = Buffer.alloc(4 + 4 + data.length + 4);
|
||||
buf.writeUInt32BE(data.length, 0);
|
||||
buf.write(type, 4, 4, "ascii");
|
||||
data.copy(buf, 8);
|
||||
const crc = crc32(buf.subarray(4, 8 + data.length));
|
||||
buf.writeUInt32BE(crc >>> 0, 8 + data.length);
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function encodePng(width: number, height: number, rgba: Buffer): Buffer {
|
||||
const rawData = Buffer.alloc(height * (1 + width * 4))
|
||||
const rawData = Buffer.alloc(height * (1 + width * 4));
|
||||
for (let y = 0; y < height; y++) {
|
||||
const rowStart = y * (1 + width * 4)
|
||||
rawData[rowStart] = 0 // filter: None
|
||||
rgba.copy(rawData, rowStart + 1, y * width * 4, (y + 1) * width * 4)
|
||||
const rowStart = y * (1 + width * 4);
|
||||
rawData[rowStart] = 0; // filter: None
|
||||
rgba.copy(rawData, rowStart + 1, y * width * 4, (y + 1) * width * 4);
|
||||
}
|
||||
|
||||
const compressed = deflateSync(rawData)
|
||||
const compressed = deflateSync(rawData);
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
// Signature
|
||||
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
|
||||
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
||||
|
||||
// IHDR
|
||||
const ihdr = Buffer.alloc(13)
|
||||
ihdr.writeUInt32BE(width, 0)
|
||||
ihdr.writeUInt32BE(height, 4)
|
||||
ihdr[8] = 8 // bit depth
|
||||
ihdr[9] = 6 // color type: RGBA
|
||||
ihdr[10] = 0 // compression
|
||||
ihdr[11] = 0 // filter
|
||||
ihdr[12] = 0 // interlace
|
||||
chunks.push(pngChunk('IHDR', ihdr))
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type: RGBA
|
||||
ihdr[10] = 0; // compression
|
||||
ihdr[11] = 0; // filter
|
||||
ihdr[12] = 0; // interlace
|
||||
chunks.push(pngChunk("IHDR", ihdr));
|
||||
|
||||
// IDAT
|
||||
chunks.push(pngChunk('IDAT', compressed))
|
||||
chunks.push(pngChunk("IDAT", compressed));
|
||||
|
||||
// IEND
|
||||
chunks.push(pngChunk('IEND', Buffer.alloc(0)))
|
||||
chunks.push(pngChunk("IEND", Buffer.alloc(0)));
|
||||
|
||||
return Buffer.concat(chunks)
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
// ── Bin-Packing Spritesheet Builder ────────────────────────────────
|
||||
|
||||
interface PackedRect {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
name: string
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function nextPow2(v: number): number {
|
||||
v--
|
||||
v |= v >> 1
|
||||
v |= v >> 2
|
||||
v |= v >> 4
|
||||
v |= v >> 8
|
||||
v |= v >> 16
|
||||
return v + 1
|
||||
v--;
|
||||
v |= v >> 1;
|
||||
v |= v >> 2;
|
||||
v |= v >> 4;
|
||||
v |= v >> 8;
|
||||
v |= v >> 16;
|
||||
return v + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,166 +95,166 @@ function nextPow2(v: number): number {
|
||||
*/
|
||||
function binPack(images: SpriteImage[]): { rects: PackedRect[]; width: number; height: number } {
|
||||
if (images.length === 0) {
|
||||
return { rects: [], width: 1, height: 1 }
|
||||
return { rects: [], width: 1, height: 1 };
|
||||
}
|
||||
|
||||
// Sort by height descending for better shelf packing
|
||||
const sorted = [...images].sort((a, b) => b.height - a.height || b.width - a.width)
|
||||
const sorted = [...images].sort((a, b) => b.height - a.height || b.width - a.width);
|
||||
|
||||
// Estimate initial atlas width based on total area
|
||||
const totalArea = sorted.reduce((sum, img) => sum + img.width * img.height, 0)
|
||||
const maxSingleWidth = Math.max(...sorted.map((i) => i.width))
|
||||
let atlasWidth = Math.max(maxSingleWidth, Math.ceil(Math.sqrt(totalArea)))
|
||||
const totalArea = sorted.reduce((sum, img) => sum + img.width * img.height, 0);
|
||||
const maxSingleWidth = Math.max(...sorted.map((i) => i.width));
|
||||
let atlasWidth = Math.max(maxSingleWidth, Math.ceil(Math.sqrt(totalArea)));
|
||||
|
||||
// Round up to power of 2 for GPU friendliness (max 2048)
|
||||
atlasWidth = Math.min(2048, nextPow2(atlasWidth))
|
||||
atlasWidth = Math.min(2048, nextPow2(atlasWidth));
|
||||
|
||||
const rects: PackedRect[] = []
|
||||
let shelfY = 0
|
||||
let shelfHeight = 0
|
||||
let shelfX = 0
|
||||
const rects: PackedRect[] = [];
|
||||
let shelfY = 0;
|
||||
let shelfHeight = 0;
|
||||
let shelfX = 0;
|
||||
|
||||
for (const img of sorted) {
|
||||
// If sprite doesn't fit on current shelf, start new shelf
|
||||
if (shelfX + img.width > atlasWidth) {
|
||||
shelfY += shelfHeight
|
||||
shelfHeight = 0
|
||||
shelfX = 0
|
||||
shelfY += shelfHeight;
|
||||
shelfHeight = 0;
|
||||
shelfX = 0;
|
||||
}
|
||||
|
||||
rects.push({ x: shelfX, y: shelfY, w: img.width, h: img.height, name: img.name })
|
||||
shelfX += img.width
|
||||
shelfHeight = Math.max(shelfHeight, img.height)
|
||||
rects.push({ x: shelfX, y: shelfY, w: img.width, h: img.height, name: img.name });
|
||||
shelfX += img.width;
|
||||
shelfHeight = Math.max(shelfHeight, img.height);
|
||||
}
|
||||
|
||||
const finalHeight = shelfY + shelfHeight
|
||||
return { rects, width: atlasWidth, height: finalHeight || 1 }
|
||||
const finalHeight = shelfY + shelfHeight;
|
||||
return { rects, width: atlasWidth, height: finalHeight || 1 };
|
||||
}
|
||||
|
||||
// ── Spritesheet Builder ────────────────────────────────────────────
|
||||
|
||||
interface FrameInfo {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export function buildSpritesheet(images: SpriteImage[]): {
|
||||
png: Buffer
|
||||
frames: Map<string, FrameInfo>
|
||||
width: number
|
||||
height: number
|
||||
png: Buffer;
|
||||
frames: Map<string, FrameInfo>;
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
if (images.length === 0) {
|
||||
const emptyPng = encodePng(1, 1, Buffer.alloc(4))
|
||||
return { png: emptyPng, frames: new Map(), width: 1, height: 1 }
|
||||
const emptyPng = encodePng(1, 1, Buffer.alloc(4));
|
||||
return { png: emptyPng, frames: new Map(), width: 1, height: 1 };
|
||||
}
|
||||
|
||||
const { rects, width: sheetWidth, height: sheetHeight } = binPack(images)
|
||||
const { rects, width: sheetWidth, height: sheetHeight } = binPack(images);
|
||||
|
||||
// Build lookup by name for pixel data
|
||||
const imageByName = new Map(images.map((img) => [img.name, img]))
|
||||
const imageByName = new Map(images.map((img) => [img.name, img]));
|
||||
|
||||
const combined = Buffer.alloc(sheetWidth * sheetHeight * 4)
|
||||
const frames = new Map<string, FrameInfo>()
|
||||
const combined = Buffer.alloc(sheetWidth * sheetHeight * 4);
|
||||
const frames = new Map<string, FrameInfo>();
|
||||
|
||||
for (const rect of rects) {
|
||||
const img = imageByName.get(rect.name)
|
||||
if (!img) continue
|
||||
const img = imageByName.get(rect.name);
|
||||
if (!img) continue;
|
||||
|
||||
// Copy pixels row by row
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const srcStart = y * img.width * 4
|
||||
const dstStart = ((rect.y + y) * sheetWidth + rect.x) * 4
|
||||
img.pixels.copy(combined, dstStart, srcStart, srcStart + img.width * 4)
|
||||
const srcStart = y * img.width * 4;
|
||||
const dstStart = ((rect.y + y) * sheetWidth + rect.x) * 4;
|
||||
img.pixels.copy(combined, dstStart, srcStart, srcStart + img.width * 4);
|
||||
}
|
||||
frames.set(rect.name, { x: rect.x, y: rect.y, w: rect.w, h: rect.h })
|
||||
frames.set(rect.name, { x: rect.x, y: rect.y, w: rect.w, h: rect.h });
|
||||
}
|
||||
|
||||
const png = encodePng(sheetWidth, sheetHeight, combined)
|
||||
return { png, frames, width: sheetWidth, height: sheetHeight }
|
||||
const png = encodePng(sheetWidth, sheetHeight, combined);
|
||||
return { png, frames, width: sheetWidth, height: sheetHeight };
|
||||
}
|
||||
|
||||
// ── .nitro Bundle Creator ──────────────────────────────────────────
|
||||
|
||||
export function createNitroBundle(jsonData: object, pngData: Buffer, name: string): Buffer {
|
||||
const jsonStr = JSON.stringify(jsonData)
|
||||
const jsonCompressed = deflateSync(Buffer.from(jsonStr, 'utf-8'))
|
||||
const pngCompressed = deflateSync(pngData)
|
||||
const jsonStr = JSON.stringify(jsonData);
|
||||
const jsonCompressed = deflateSync(Buffer.from(jsonStr, "utf-8"));
|
||||
const pngCompressed = deflateSync(pngData);
|
||||
|
||||
const jsonFileName = `${name}.json`
|
||||
const pngFileName = `${name}.png`
|
||||
const jsonFileName = `${name}.json`;
|
||||
const pngFileName = `${name}.png`;
|
||||
|
||||
const totalSize =
|
||||
2 +
|
||||
(2 + jsonFileName.length + 4 + jsonCompressed.length) +
|
||||
(2 + pngFileName.length + 4 + pngCompressed.length)
|
||||
(2 + pngFileName.length + 4 + pngCompressed.length);
|
||||
|
||||
const bundle = Buffer.alloc(totalSize)
|
||||
let offset = 0
|
||||
const bundle = Buffer.alloc(totalSize);
|
||||
let offset = 0;
|
||||
|
||||
bundle.writeInt16BE(2, offset)
|
||||
offset += 2
|
||||
bundle.writeInt16BE(2, offset);
|
||||
offset += 2;
|
||||
|
||||
bundle.writeInt16BE(jsonFileName.length, offset)
|
||||
offset += 2
|
||||
bundle.write(jsonFileName, offset, jsonFileName.length, 'utf-8')
|
||||
offset += jsonFileName.length
|
||||
bundle.writeInt32BE(jsonCompressed.length, offset)
|
||||
offset += 4
|
||||
jsonCompressed.copy(bundle, offset)
|
||||
offset += jsonCompressed.length
|
||||
bundle.writeInt16BE(jsonFileName.length, offset);
|
||||
offset += 2;
|
||||
bundle.write(jsonFileName, offset, jsonFileName.length, "utf-8");
|
||||
offset += jsonFileName.length;
|
||||
bundle.writeInt32BE(jsonCompressed.length, offset);
|
||||
offset += 4;
|
||||
jsonCompressed.copy(bundle, offset);
|
||||
offset += jsonCompressed.length;
|
||||
|
||||
bundle.writeInt16BE(pngFileName.length, offset)
|
||||
offset += 2
|
||||
bundle.write(pngFileName, offset, pngFileName.length, 'utf-8')
|
||||
offset += pngFileName.length
|
||||
bundle.writeInt32BE(pngCompressed.length, offset)
|
||||
offset += 4
|
||||
pngCompressed.copy(bundle, offset)
|
||||
bundle.writeInt16BE(pngFileName.length, offset);
|
||||
offset += 2;
|
||||
bundle.write(pngFileName, offset, pngFileName.length, "utf-8");
|
||||
offset += pngFileName.length;
|
||||
bundle.writeInt32BE(pngCompressed.length, offset);
|
||||
offset += 4;
|
||||
pngCompressed.copy(bundle, offset);
|
||||
|
||||
return bundle
|
||||
return bundle;
|
||||
}
|
||||
|
||||
// ── .nitro Bundle Parser ──────────────────────────────────────────
|
||||
|
||||
export function parseNitroBundle(buffer: Buffer): {
|
||||
json: Record<string, unknown>
|
||||
jsonFileName: string
|
||||
png: Buffer
|
||||
pngFileName: string
|
||||
json: Record<string, unknown>;
|
||||
jsonFileName: string;
|
||||
png: Buffer;
|
||||
pngFileName: string;
|
||||
} {
|
||||
let offset = 0
|
||||
let offset = 0;
|
||||
|
||||
const fileCount = buffer.readInt16BE(offset)
|
||||
offset += 2
|
||||
const fileCount = buffer.readInt16BE(offset);
|
||||
offset += 2;
|
||||
if (fileCount < 2) {
|
||||
throw new Error(`Expected at least 2 files in .nitro bundle, got ${fileCount}`)
|
||||
throw new Error(`Expected at least 2 files in .nitro bundle, got ${fileCount}`);
|
||||
}
|
||||
|
||||
// File 1: JSON
|
||||
const jsonFileNameLen = buffer.readInt16BE(offset)
|
||||
offset += 2
|
||||
const jsonFileName = buffer.toString('utf-8', offset, offset + jsonFileNameLen)
|
||||
offset += jsonFileNameLen
|
||||
const jsonCompressedLen = buffer.readInt32BE(offset)
|
||||
offset += 4
|
||||
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen)
|
||||
offset += jsonCompressedLen
|
||||
const jsonStr = inflateSync(jsonCompressed).toString('utf-8')
|
||||
const json = JSON.parse(jsonStr)
|
||||
const jsonFileNameLen = buffer.readInt16BE(offset);
|
||||
offset += 2;
|
||||
const jsonFileName = buffer.toString("utf-8", offset, offset + jsonFileNameLen);
|
||||
offset += jsonFileNameLen;
|
||||
const jsonCompressedLen = buffer.readInt32BE(offset);
|
||||
offset += 4;
|
||||
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen);
|
||||
offset += jsonCompressedLen;
|
||||
const jsonStr = inflateSync(jsonCompressed).toString("utf-8");
|
||||
const json = JSON.parse(jsonStr);
|
||||
|
||||
// File 2: PNG
|
||||
const pngFileNameLen = buffer.readInt16BE(offset)
|
||||
offset += 2
|
||||
const pngFileName = buffer.toString('utf-8', offset, offset + pngFileNameLen)
|
||||
offset += pngFileNameLen
|
||||
const pngCompressedLen = buffer.readInt32BE(offset)
|
||||
offset += 4
|
||||
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen)
|
||||
offset += pngCompressedLen
|
||||
const png = inflateSync(pngCompressed)
|
||||
const pngFileNameLen = buffer.readInt16BE(offset);
|
||||
offset += 2;
|
||||
const pngFileName = buffer.toString("utf-8", offset, offset + pngFileNameLen);
|
||||
offset += pngFileNameLen;
|
||||
const pngCompressedLen = buffer.readInt32BE(offset);
|
||||
offset += 4;
|
||||
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen);
|
||||
offset += pngCompressedLen;
|
||||
const png = inflateSync(pngCompressed);
|
||||
|
||||
return { json, jsonFileName, png, pngFileName }
|
||||
return { json, jsonFileName, png, pngFileName };
|
||||
}
|
||||
@@ -2,151 +2,151 @@
|
||||
* Low-level SWF file parsing: decompression, tag reading, symbol class.
|
||||
*/
|
||||
|
||||
import { inflateSync } from 'node:zlib'
|
||||
import LZMA from 'lzma'
|
||||
import { inflateSync } from "node:zlib";
|
||||
import LZMA from "lzma";
|
||||
|
||||
// ── SWF Tag Types ──────────────────────────────────────────────────
|
||||
export const TAG_END = 0
|
||||
export const TAG_DEFINE_BITS_LOSSLESS = 20
|
||||
export const TAG_DEFINE_BITS_JPEG2 = 21
|
||||
export const TAG_DEFINE_BITS_JPEG3 = 35
|
||||
export const TAG_DEFINE_BITS_LOSSLESS2 = 36
|
||||
export const TAG_SYMBOL_CLASS = 76
|
||||
export const TAG_DEFINE_BINARY_DATA = 87
|
||||
export const TAG_END = 0;
|
||||
export const TAG_DEFINE_BITS_LOSSLESS = 20;
|
||||
export const TAG_DEFINE_BITS_JPEG2 = 21;
|
||||
export const TAG_DEFINE_BITS_JPEG3 = 35;
|
||||
export const TAG_DEFINE_BITS_LOSSLESS2 = 36;
|
||||
export const TAG_SYMBOL_CLASS = 76;
|
||||
export const TAG_DEFINE_BINARY_DATA = 87;
|
||||
|
||||
// ── Interfaces ─────────────────────────────────────────────────────
|
||||
|
||||
export interface SwfTag {
|
||||
type: number
|
||||
data: Buffer
|
||||
type: number;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
export interface SymbolClassResult {
|
||||
/** charID → first symbol name (canonical name) */
|
||||
classNames: Map<number, string>
|
||||
classNames: Map<number, string>;
|
||||
/** symbol name → charID (all symbols including aliases) */
|
||||
symbols: Map<string, number>
|
||||
symbols: Map<string, number>;
|
||||
}
|
||||
|
||||
// ── SWF Header Parsing ─────────────────────────────────────────────
|
||||
|
||||
export function decompressSwf(buffer: Buffer): Buffer {
|
||||
const sig = buffer.toString('ascii', 0, 3)
|
||||
const sig = buffer.toString("ascii", 0, 3);
|
||||
|
||||
if (sig === 'FWS') {
|
||||
return buffer
|
||||
if (sig === "FWS") {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
if (sig === 'CWS') {
|
||||
const header = Buffer.alloc(8)
|
||||
buffer.copy(header, 0, 0, 8)
|
||||
header.write('FWS', 0, 3, 'ascii')
|
||||
const compressed = buffer.subarray(8)
|
||||
const decompressed = inflateSync(compressed)
|
||||
return Buffer.concat([header, decompressed])
|
||||
if (sig === "CWS") {
|
||||
const header = Buffer.alloc(8);
|
||||
buffer.copy(header, 0, 0, 8);
|
||||
header.write("FWS", 0, 3, "ascii");
|
||||
const compressed = buffer.subarray(8);
|
||||
const decompressed = inflateSync(compressed);
|
||||
return Buffer.concat([header, decompressed]);
|
||||
}
|
||||
|
||||
if (sig === 'ZWS') {
|
||||
const fileLength = buffer.readUInt32LE(4)
|
||||
const compressedSize = buffer.readUInt32LE(8)
|
||||
const lzmaData = buffer.subarray(12, 12 + compressedSize)
|
||||
if (sig === "ZWS") {
|
||||
const fileLength = buffer.readUInt32LE(4);
|
||||
const compressedSize = buffer.readUInt32LE(8);
|
||||
const lzmaData = buffer.subarray(12, 12 + compressedSize);
|
||||
|
||||
const props = lzmaData.subarray(0, 5)
|
||||
const compressedPayload = lzmaData.subarray(5)
|
||||
const props = lzmaData.subarray(0, 5);
|
||||
const compressedPayload = lzmaData.subarray(5);
|
||||
|
||||
const uncompressedSize = fileLength - 8
|
||||
const lzmaHeader = Buffer.alloc(13)
|
||||
props.copy(lzmaHeader, 0)
|
||||
lzmaHeader.writeUInt32LE(uncompressedSize, 5)
|
||||
lzmaHeader.writeUInt32LE(0, 9)
|
||||
const uncompressedSize = fileLength - 8;
|
||||
const lzmaHeader = Buffer.alloc(13);
|
||||
props.copy(lzmaHeader, 0);
|
||||
lzmaHeader.writeUInt32LE(uncompressedSize, 5);
|
||||
lzmaHeader.writeUInt32LE(0, 9);
|
||||
|
||||
const lzmaStream = Buffer.concat([lzmaHeader, compressedPayload])
|
||||
const decompressed = Buffer.from(LZMA.decompress(lzmaStream) as number[])
|
||||
const lzmaStream = Buffer.concat([lzmaHeader, compressedPayload]);
|
||||
const decompressed = Buffer.from(LZMA.decompress(lzmaStream) as number[]);
|
||||
|
||||
const header = Buffer.alloc(8)
|
||||
buffer.copy(header, 0, 0, 8)
|
||||
header.write('FWS', 0, 3, 'ascii')
|
||||
return Buffer.concat([header, decompressed])
|
||||
const header = Buffer.alloc(8);
|
||||
buffer.copy(header, 0, 0, 8);
|
||||
header.write("FWS", 0, 3, "ascii");
|
||||
return Buffer.concat([header, decompressed]);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown SWF signature: ${sig}`)
|
||||
throw new Error(`Unknown SWF signature: ${sig}`);
|
||||
}
|
||||
|
||||
function readRect(buf: Buffer, offset: number): number {
|
||||
const nbits = (buf[offset] >> 3) & 0x1f
|
||||
const totalBits = 5 + nbits * 4
|
||||
const totalBytes = Math.ceil(totalBits / 8)
|
||||
return offset + totalBytes
|
||||
const nbits = (buf[offset] >> 3) & 0x1f;
|
||||
const totalBits = 5 + nbits * 4;
|
||||
const totalBytes = Math.ceil(totalBits / 8);
|
||||
return offset + totalBytes;
|
||||
}
|
||||
|
||||
// ── SWF Tag Reader ─────────────────────────────────────────────────
|
||||
|
||||
export function parseSwfTags(swfBuffer: Buffer): SwfTag[] {
|
||||
const tags: SwfTag[] = []
|
||||
const tags: SwfTag[] = [];
|
||||
|
||||
let offset = 8
|
||||
offset = readRect(swfBuffer, offset)
|
||||
offset += 4
|
||||
let offset = 8;
|
||||
offset = readRect(swfBuffer, offset);
|
||||
offset += 4;
|
||||
|
||||
while (offset < swfBuffer.length) {
|
||||
if (offset + 2 > swfBuffer.length) break
|
||||
if (offset + 2 > swfBuffer.length) break;
|
||||
|
||||
const tagCodeAndLength = swfBuffer.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const tagCodeAndLength = swfBuffer.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
const tagType = (tagCodeAndLength >> 6) & 0x3ff
|
||||
let tagLength = tagCodeAndLength & 0x3f
|
||||
const tagType = (tagCodeAndLength >> 6) & 0x3ff;
|
||||
let tagLength = tagCodeAndLength & 0x3f;
|
||||
|
||||
if (tagLength === 0x3f) {
|
||||
if (offset + 4 > swfBuffer.length) break
|
||||
tagLength = swfBuffer.readUInt32LE(offset)
|
||||
offset += 4
|
||||
if (offset + 4 > swfBuffer.length) break;
|
||||
tagLength = swfBuffer.readUInt32LE(offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
if (tagType === TAG_END) break
|
||||
if (offset + tagLength > swfBuffer.length) break
|
||||
if (tagType === TAG_END) break;
|
||||
if (offset + tagLength > swfBuffer.length) break;
|
||||
|
||||
const tagData = swfBuffer.subarray(offset, offset + tagLength)
|
||||
tags.push({ type: tagType, data: Buffer.from(tagData) })
|
||||
offset += tagLength
|
||||
const tagData = swfBuffer.subarray(offset, offset + tagLength);
|
||||
tags.push({ type: tagType, data: Buffer.from(tagData) });
|
||||
offset += tagLength;
|
||||
}
|
||||
|
||||
return tags
|
||||
return tags;
|
||||
}
|
||||
|
||||
// ── SymbolClass Parser ─────────────────────────────────────────────
|
||||
|
||||
export function parseSymbolClass(data: Buffer): SymbolClassResult {
|
||||
const classNames = new Map<number, string>()
|
||||
const symbols = new Map<string, number>()
|
||||
let offset = 0
|
||||
const classNames = new Map<number, string>();
|
||||
const symbols = new Map<string, number>();
|
||||
let offset = 0;
|
||||
|
||||
const numSymbols = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
const numSymbols = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
for (let i = 0; i < numSymbols; i++) {
|
||||
if (offset + 2 > data.length) break
|
||||
const charId = data.readUInt16LE(offset)
|
||||
offset += 2
|
||||
if (offset + 2 > data.length) break;
|
||||
const charId = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
const strStart = offset
|
||||
while (offset < data.length && data[offset] !== 0) offset++
|
||||
const name = data.toString('utf-8', strStart, offset)
|
||||
offset++
|
||||
const strStart = offset;
|
||||
while (offset < data.length && data[offset] !== 0) offset++;
|
||||
const name = data.toString("utf-8", strStart, offset);
|
||||
offset++;
|
||||
|
||||
symbols.set(name, charId)
|
||||
symbols.set(name, charId);
|
||||
if (!classNames.has(charId)) {
|
||||
classNames.set(charId, name)
|
||||
classNames.set(charId, name);
|
||||
}
|
||||
}
|
||||
|
||||
return { classNames, symbols }
|
||||
return { classNames, symbols };
|
||||
}
|
||||
|
||||
// ── DefineBinaryData Parser ────────────────────────────────────────
|
||||
|
||||
export function parseBinaryData(data: Buffer): { id: number; content: Buffer } {
|
||||
const id = data.readUInt16LE(0)
|
||||
const content = data.subarray(6)
|
||||
return { id, content }
|
||||
const id = data.readUInt16LE(0);
|
||||
const content = data.subarray(6);
|
||||
return { id, content };
|
||||
}
|
||||
@@ -7,575 +7,569 @@
|
||||
// ── Interfaces ─────────────────────────────────────────────────────
|
||||
|
||||
export interface AssetOffset {
|
||||
x: number
|
||||
y: number
|
||||
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 }[]
|
||||
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
|
||||
tag: string;
|
||||
attrs: Record<string, string>;
|
||||
children: XmlNode[];
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function parseXml(xml: string): XmlNode | null {
|
||||
const stack: XmlNode[] = []
|
||||
const nodes: XmlNode[] = []
|
||||
const stack: XmlNode[] = [];
|
||||
const nodes: XmlNode[] = [];
|
||||
|
||||
// Remove XML declaration and comments
|
||||
const clean = xml
|
||||
.replace(/<\?[^?]*\?>/g, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.trim()
|
||||
.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
|
||||
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 fullMatch = match[0];
|
||||
const tagName = match[1];
|
||||
const attrsStr = match[2] || "";
|
||||
const matchIndex = match.index ?? 0;
|
||||
|
||||
const text = clean.substring(lastIndex, matchIndex).trim()
|
||||
const text = clean.substring(lastIndex, matchIndex).trim();
|
||||
if (text && stack.length > 0) {
|
||||
stack[stack.length - 1].text += text
|
||||
stack[stack.length - 1].text += text;
|
||||
}
|
||||
lastIndex = matchIndex + fullMatch.length
|
||||
lastIndex = matchIndex + fullMatch.length;
|
||||
|
||||
if (fullMatch.startsWith('</')) {
|
||||
if (fullMatch.startsWith("</")) {
|
||||
if (stack.length > 0) {
|
||||
const node = stack.pop()!
|
||||
const node = stack.pop()!;
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1].children.push(node)
|
||||
stack[stack.length - 1].children.push(node);
|
||||
} else {
|
||||
nodes.push(node)
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const attrs: Record<string, string> = {}
|
||||
const attrRe = /([a-zA-Z_][\w.-]*)\s*=\s*"([^"]*)"/g
|
||||
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]
|
||||
attrs[am[1]] = am[2];
|
||||
}
|
||||
|
||||
const node: XmlNode = { tag: tagName, attrs, children: [], text: '' }
|
||||
const node: XmlNode = { tag: tagName, attrs, children: [], text: "" };
|
||||
|
||||
if (fullMatch.endsWith('/>')) {
|
||||
if (fullMatch.endsWith("/>")) {
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1].children.push(node)
|
||||
stack[stack.length - 1].children.push(node);
|
||||
} else {
|
||||
nodes.push(node)
|
||||
nodes.push(node);
|
||||
}
|
||||
} else {
|
||||
stack.push(node)
|
||||
stack.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (stack.length > 1) {
|
||||
const node = stack.pop()!
|
||||
stack[stack.length - 1].children.push(node)
|
||||
const node = stack.pop()!;
|
||||
stack[stack.length - 1].children.push(node);
|
||||
}
|
||||
if (stack.length === 1) nodes.push(stack[0])
|
||||
if (stack.length === 1) nodes.push(stack[0]);
|
||||
|
||||
return nodes[0] || null
|
||||
return nodes[0] || null;
|
||||
}
|
||||
|
||||
export function findChild(node: XmlNode, tag: string): XmlNode | undefined {
|
||||
return node.children.find((c) => c.tag === tag)
|
||||
return node.children.find((c) => c.tag === tag);
|
||||
}
|
||||
|
||||
export function findChildren(node: XmlNode, tag: string): XmlNode[] {
|
||||
return node.children.filter((c) => c.tag === tag)
|
||||
return node.children.filter((c) => c.tag === tag);
|
||||
}
|
||||
|
||||
// ── XML Data Processors ────────────────────────────────────────────
|
||||
|
||||
export interface ManifestAsset {
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
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 assets: ManifestAsset[] = [];
|
||||
const root = parseXml(xml);
|
||||
if (!root) return { assets };
|
||||
|
||||
const libNode = root.tag === 'manifest' ? findChild(root, 'library') : root
|
||||
if (!libNode) return { assets }
|
||||
const libNode = root.tag === "manifest" ? findChild(root, "library") : root;
|
||||
if (!libNode) return { assets };
|
||||
|
||||
const assetsNode = findChild(libNode, 'assets')
|
||||
if (!assetsNode) 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
|
||||
for (const asset of findChildren(assetsNode, "asset")) {
|
||||
const name = asset.attrs.name || "";
|
||||
if (!name) continue;
|
||||
// Skip shadow sprites
|
||||
if (name.startsWith('sh_')) continue
|
||||
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)
|
||||
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 })
|
||||
assets.push({ name, x, y });
|
||||
}
|
||||
|
||||
return { assets }
|
||||
return { assets };
|
||||
}
|
||||
|
||||
export interface PaletteEntry {
|
||||
id: number
|
||||
source?: string
|
||||
master?: boolean
|
||||
tags?: string[]
|
||||
breed?: number
|
||||
colorTag?: number
|
||||
color1?: string
|
||||
color2?: string
|
||||
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>
|
||||
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 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
|
||||
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
|
||||
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)
|
||||
const x = parseInt(asset.attrs.x || "0", 10);
|
||||
const y = parseInt(asset.attrs.y || "0", 10);
|
||||
|
||||
offsets.set(name, { x, y })
|
||||
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
|
||||
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 }
|
||||
return { offsets, palettes };
|
||||
}
|
||||
|
||||
export function processIndex(
|
||||
xml: string,
|
||||
): { visualizationType: string; logicType: string; specialtype: number } | null {
|
||||
const root = parseXml(xml)
|
||||
if (!root) return null
|
||||
const root = parseXml(xml);
|
||||
if (!root) return null;
|
||||
|
||||
const obj = root.tag === 'object' ? root : findChild(root, 'object')
|
||||
if (!obj) 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),
|
||||
}
|
||||
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>[]
|
||||
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: '',
|
||||
maskType: "",
|
||||
canstandon: false,
|
||||
cansiton: false,
|
||||
canlayon: false,
|
||||
}
|
||||
const root = parseXml(xml)
|
||||
if (!root) return defaults
|
||||
};
|
||||
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 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'),
|
||||
}
|
||||
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)
|
||||
dimensions.centerZ = parseFloat(dims.attrs.centerZ);
|
||||
}
|
||||
|
||||
const directions: number[] = []
|
||||
const directions: number[] = [];
|
||||
if (dirsNode) {
|
||||
for (const d of findChildren(dirsNode, 'direction')) {
|
||||
directions.push(parseInt(d.attrs.id || '0', 10))
|
||||
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)
|
||||
if (directions.length === 0) directions.push(0, 90);
|
||||
|
||||
// Parse mask type
|
||||
const maskNode = findChild(root, 'mask')
|
||||
const maskType = maskNode?.attrs.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 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 }
|
||||
const result: LogicResult = { dimensions, directions, maskType, canstandon, cansiton, canlayon };
|
||||
|
||||
// Parse action link/startState
|
||||
const actionNode = findChild(root, 'action')
|
||||
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
|
||||
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')
|
||||
const creditsNode = findChild(root, "credits");
|
||||
if (creditsNode?.attrs.value) {
|
||||
result.credits = creditsNode.attrs.value
|
||||
result.credits = creditsNode.attrs.value;
|
||||
}
|
||||
|
||||
// Parse soundSample
|
||||
const soundNode = findChild(root, 'sound')
|
||||
const soundNode = findChild(root, "sound");
|
||||
if (soundNode) {
|
||||
const sampleNode = findChild(soundNode, 'sample')
|
||||
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
|
||||
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')
|
||||
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)
|
||||
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 }
|
||||
if (variables.length > 0) result.customVars = { variables };
|
||||
}
|
||||
|
||||
// Parse particleSystems
|
||||
const particleNode = findChild(root, '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 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 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')
|
||||
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 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)
|
||||
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 (pFrames.length > 0) particle.frames = pFrames;
|
||||
particles.push(particle);
|
||||
}
|
||||
if (particles.length > 0) emitter.particles = particles
|
||||
emitters.push(emitter)
|
||||
if (particles.length > 0) emitter.particles = particles;
|
||||
emitters.push(emitter);
|
||||
}
|
||||
if (emitters.length > 0) system.emitters = emitters
|
||||
systems.push(system)
|
||||
if (emitters.length > 0) system.emitters = emitters;
|
||||
systems.push(system);
|
||||
}
|
||||
if (systems.length > 0) result.particleSystems = systems
|
||||
if (systems.length > 0) result.particleSystems = systems;
|
||||
}
|
||||
|
||||
// Parse planetSystems
|
||||
const planetNode = findChild(root, 'planetSystems')
|
||||
const planetNode = findChild(root, "planetSystems");
|
||||
if (planetNode) {
|
||||
const systems: Record<string, unknown>[] = []
|
||||
const systems: Record<string, unknown>[] = [];
|
||||
for (const ps of planetNode.children) {
|
||||
const system: Record<string, unknown> = { ...ps.attrs }
|
||||
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)
|
||||
const val = system[key] as string;
|
||||
if (/^-?\d+(\.\d+)?$/.test(val)) system[key] = parseFloat(val);
|
||||
}
|
||||
systems.push(system)
|
||||
systems.push(system);
|
||||
}
|
||||
if (systems.length > 0) result.planetSystems = systems
|
||||
if (systems.length > 0) result.planetSystems = systems;
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
export function processVisualization(xml: string): NitroVisualization[] {
|
||||
const result: NitroVisualization[] = []
|
||||
const root = parseXml(xml)
|
||||
if (!root) return result
|
||||
const result: NitroVisualization[] = [];
|
||||
const root = parseXml(xml);
|
||||
if (!root) return result;
|
||||
|
||||
const graphics = findChild(root, 'graphics') || root
|
||||
const graphics = findChild(root, "graphics") || root;
|
||||
|
||||
for (const viz of findChildren(graphics, 'visualization')) {
|
||||
const size = parseInt(viz.attrs.size || '64', 10)
|
||||
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
|
||||
if (size === 32) continue;
|
||||
|
||||
const layerCount = parseInt(viz.attrs.layerCount || '1', 10)
|
||||
const angle = parseInt(viz.attrs.angle || '45', 10)
|
||||
const layerCount = parseInt(viz.attrs.layerCount || "1", 10);
|
||||
const angle = parseInt(viz.attrs.angle || "45", 10);
|
||||
|
||||
const entry: NitroVisualization = { layerCount, angle, size }
|
||||
const entry: NitroVisualization = { layerCount, angle, size };
|
||||
|
||||
// Layers (before directions to match reference key order)
|
||||
const layersNode = findChild(viz, 'layers')
|
||||
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
|
||||
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
|
||||
if (Object.keys(layers).length > 0) entry.layers = layers;
|
||||
}
|
||||
|
||||
// Directions
|
||||
const dirsNode = findChild(viz, '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')
|
||||
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>> = {}
|
||||
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
|
||||
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 }
|
||||
dirs[id] = { layers: layerData };
|
||||
} else {
|
||||
dirs[id] = {}
|
||||
dirs[id] = {};
|
||||
}
|
||||
}
|
||||
entry.directions = dirs
|
||||
entry.directions = dirs;
|
||||
}
|
||||
|
||||
// Colors
|
||||
const colorsNode = findChild(viz, '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) }
|
||||
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 }
|
||||
colors[cid] = { layers: colorLayers };
|
||||
}
|
||||
if (Object.keys(colors).length > 0) entry.colors = colors
|
||||
if (Object.keys(colors).length > 0) entry.colors = colors;
|
||||
}
|
||||
|
||||
// Animations
|
||||
const animsNode = findChild(viz, '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 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)
|
||||
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')
|
||||
const offsetNodes = findChildren(f, "offset");
|
||||
if (offsetNodes.length > 0) {
|
||||
const offsets: Record<string, { direction: number; x: number; y: number }> = {}
|
||||
const offsets: Record<string, { direction: number; x: number; y: number }> = {};
|
||||
for (let oi = 0; oi < offsetNodes.length; oi++) {
|
||||
const o = offsetNodes[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),
|
||||
}
|
||||
direction: parseInt(o.attrs.direction || "0", 10),
|
||||
x: parseInt(o.attrs.x || "0", 10),
|
||||
y: parseInt(o.attrs.y || "0", 10),
|
||||
};
|
||||
}
|
||||
frameObj.offsets = offsets
|
||||
frameObj.offsets = offsets;
|
||||
}
|
||||
frames[String(fi)] = frameObj
|
||||
fi++
|
||||
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 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),
|
||||
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
|
||||
};
|
||||
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
|
||||
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
|
||||
if (Object.keys(animations).length > 0) entry.animations = animations;
|
||||
}
|
||||
|
||||
// Postures
|
||||
const posturesNode = findChild(viz, 'postures')
|
||||
const posturesNode = findChild(viz, "postures");
|
||||
if (posturesNode) {
|
||||
const posturesList: { id: string; animationId: number }[] = []
|
||||
for (const p of findChildren(posturesNode, 'posture')) {
|
||||
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),
|
||||
})
|
||||
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
|
||||
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')
|
||||
const gesturesNode = findChild(viz, "gestures");
|
||||
if (gesturesNode) {
|
||||
const gesturesList: { id: string; animationId: number }[] = []
|
||||
for (const g of findChildren(gesturesNode, 'gesture')) {
|
||||
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),
|
||||
})
|
||||
id: g.attrs.id || "",
|
||||
animationId: parseInt(g.attrs.animationId || "0", 10),
|
||||
});
|
||||
}
|
||||
if (gesturesList.length > 0) entry.gestures = gesturesList
|
||||
if (gesturesList.length > 0) entry.gestures = gesturesList;
|
||||
}
|
||||
|
||||
result.push(entry)
|
||||
result.push(entry);
|
||||
}
|
||||
|
||||
return result
|
||||
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) }
|
||||
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
|
||||
if (directions.includes(2)) return 2;
|
||||
if (directions.includes(0)) return 0;
|
||||
return directions[0] || 0;
|
||||
}
|
||||
Reference in new issue
Block a user