style: format code biome
Local Build and Deploy / deploy (push) Failing after 46s

This commit is contained in:
openhands committed 2026-07-13 21:57:41 +02:00
1 parent 8efd032cc6
commit df38dccbf1
735 files changed
+128321 -120870

No files matched your search

+252 -239
View File
@@ -2,302 +2,315 @@ 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;
if (format === 3) {
colorTableSize = data.readUInt8(offset) + 1;
offset += 1;
}
let colorTableSize = 0;
if (format === 3) {
colorTableSize = data.readUInt8(offset) + 1;
offset += 1;
}
const compressed = data.subarray(offset);
let decompressed: Buffer;
try {
decompressed = inflateSync(compressed);
} catch {
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
}
const compressed = data.subarray(offset);
let decompressed: Buffer;
try {
decompressed = inflateSync(compressed);
} catch {
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;
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 (format === 5) {
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;
// 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));
}
// 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));
}
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;
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];
// 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));
}
palette.push([r, g, b, a]);
palOff += 4;
}
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;
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];
// 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));
}
palette.push([r, g, b, a]);
palOff += 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];
}
srcOff += rowPadded;
}
}
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];
}
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;
if (format === 3) {
colorTableSize = data.readUInt8(offset) + 1;
offset += 1;
}
let colorTableSize = 0;
if (format === 3) {
colorTableSize = data.readUInt8(offset) + 1;
offset += 1;
}
const compressed = data.subarray(offset);
let decompressed: Buffer;
try {
decompressed = inflateSync(compressed);
} catch {
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
}
const compressed = data.subarray(offset);
let decompressed: Buffer;
try {
decompressed = inflateSync(compressed);
} catch {
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;
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;
}
}
} else if (format === 3) {
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;
}
if (format === 5) {
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;
}
}
} else if (format === 3) {
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 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];
}
srcOff += rowPadded;
}
}
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];
}
srcOff += rowPadded;
}
}
return { id, width, height, pixels };
return { id, width, height, pixels };
}
// ── DefineBitsJpeg2 Parser (JPEG without alpha) ───────────────────
export function parseBitsJpeg2(
data: Buffer,
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 (
jpegData.length >= 4 &&
jpegData[0] === 0xff &&
jpegData[1] === 0xd9 &&
jpegData[2] === 0xff &&
jpegData[3] === 0xd1
) {
jpegData = jpegData.subarray(4);
}
// Strip erroneous JPEG header (FF D9 FF D1) if present
if (
jpegData.length >= 4 &&
jpegData[0] === 0xff &&
jpegData[1] === 0xd9 &&
jpegData[2] === 0xff &&
jpegData[3] === 0xd1
) {
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) ──────────────
export function parseBitsJpeg3(
data: Buffer,
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 (
jpegData.length >= 4 &&
jpegData[0] === 0xff &&
jpegData[1] === 0xd9 &&
jpegData[2] === 0xff &&
jpegData[3] === 0xd1
) {
jpegData = jpegData.subarray(4);
}
// Strip erroneous JPEG header (FF D9 FF D1) if present
if (
jpegData.length >= 4 &&
jpegData[0] === 0xff &&
jpegData[1] === 0xd9 &&
jpegData[2] === 0xff &&
jpegData[3] === 0xd1
) {
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);
if (compressedAlpha.length > 0) {
try {
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];
}
} catch {
// Alpha decompression failed, keep fully opaque
}
}
// Apply zlib-compressed alpha channel
const compressedAlpha = data.subarray(6 + alphaDataOffset);
if (compressedAlpha.length > 0) {
try {
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];
}
} 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;
while (offset < jpeg.length - 1) {
if (jpeg[offset] !== 0xff) {
offset++;
continue;
}
export function parseJpegDimensions(
jpeg: Buffer,
): { width: number; height: number } | null {
let offset = 0;
while (offset < jpeg.length - 1) {
if (jpeg[offset] !== 0xff) {
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;
// Skip length (2 bytes) + precision (1 byte)
const height = jpeg.readUInt16BE(offset + 3);
const width = jpeg.readUInt16BE(offset + 5);
return { width, height };
}
// SOF0 (Baseline), SOF1 (Extended), SOF2 (Progressive)
if (marker >= 0xc0 && marker <= 0xc2) {
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 };
}
// 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;
}
return null;
// 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;
}
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 });
// jpeg-js returns RGBA data directly
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);
}
export function decodeJpegToRGBA(
jpegData: Buffer,
width: number,
height: number,
): Buffer {
try {
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,
);
} catch {
// Fallback: transparent pixels if JPEG decode fails
return Buffer.alloc(width * height * 4);
}
}
+499 -398
View File
@@ -6,46 +6,54 @@
* by the Nitro HTML5 client.
*/
import { gunzipSync, inflateSync } from "node:zlib";
import type { BatchConversionItem, BatchConversionResult, ConversionResult } from "@/types/furni";
import type {
BatchConversionItem,
BatchConversionResult,
ConversionResult,
} from "@/types/furni";
import {
parseBitsJpeg2,
parseBitsJpeg3,
parseBitsLossless,
parseBitsLossless2,
type SpriteImage,
parseBitsJpeg2,
parseBitsJpeg3,
parseBitsLossless,
parseBitsLossless2,
type SpriteImage,
} from "./image-decoder";
import { buildSpritesheet, createNitroBundle, encodePng } from "./nitro-builder";
import {
decompressSwf,
parseBinaryData,
parseSwfTags,
parseSymbolClass,
type SymbolClassResult,
TAG_DEFINE_BINARY_DATA,
TAG_DEFINE_BITS_JPEG2,
TAG_DEFINE_BITS_JPEG3,
TAG_DEFINE_BITS_LOSSLESS,
TAG_DEFINE_BITS_LOSSLESS2,
TAG_SYMBOL_CLASS,
buildSpritesheet,
createNitroBundle,
encodePng,
} from "./nitro-builder";
import {
decompressSwf,
parseBinaryData,
parseSwfTags,
parseSymbolClass,
type SymbolClassResult,
TAG_DEFINE_BINARY_DATA,
TAG_DEFINE_BITS_JPEG2,
TAG_DEFINE_BITS_JPEG3,
TAG_DEFINE_BITS_LOSSLESS,
TAG_DEFINE_BITS_LOSSLESS2,
TAG_SYMBOL_CLASS,
} from "./swf-parser";
import {
computeDefaultDir,
extractPartColors,
findChild,
findChildren,
parseXml,
processAssetsXml,
processIndex,
processLogic,
processManifest,
processVisualization,
computeDefaultDir,
extractPartColors,
findChild,
findChildren,
parseXml,
processAssetsXml,
processIndex,
processLogic,
processManifest,
processVisualization,
} from "./xml-processor";
export type {
BatchConversionItem,
BatchConversionResult,
ConversionResult,
FurniMetadata,
BatchConversionItem,
BatchConversionResult,
ConversionResult,
FurniMetadata,
} from "@/types/furni";
// Re-export public API from submodules
export { createNitroBundle, parseNitroBundle } from "./nitro-builder";
@@ -53,435 +61,528 @@ export { computeDefaultDir, extractPartColors } from "./xml-processor";
// ── Main Conversion ────────────────────────────────────────────────
export function convertSwfToNitro(swfBuffer: Buffer, classname: string): ConversionResult {
const warnings: string[] = [];
export function convertSwfToNitro(
swfBuffer: Buffer,
classname: string,
): ConversionResult {
const warnings: string[] = [];
// 1. Decompress SWF
let decompressed: Buffer;
try {
decompressed = decompressSwf(swfBuffer);
} catch (err) {
throw new Error(`SWF decompression failed for ${classname}: ${(err as Error).message}`);
}
// 1. Decompress SWF
let decompressed: Buffer;
try {
decompressed = decompressSwf(swfBuffer);
} catch (err) {
throw new Error(
`SWF decompression failed for ${classname}: ${(err as Error).message}`,
);
}
// 2. Parse tags
const tags = parseSwfTags(decompressed);
if (tags.length === 0) {
throw new Error(`No tags found in SWF for ${classname}`);
}
// 2. Parse tags
const tags = parseSwfTags(decompressed);
if (tags.length === 0) {
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() };
for (const tag of tags) {
if (tag.type === TAG_SYMBOL_CLASS) {
symbolResult = parseSymbolClass(tag.data);
break;
}
}
const { classNames: symbolMap, symbols: allSymbols } = symbolResult;
// 3. Parse SymbolClass to map IDs to names
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;
}
}
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 }>();
// 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 }
>();
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;
}
case TAG_DEFINE_BITS_LOSSLESS2: {
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;
}
case TAG_DEFINE_BITS_JPEG2: {
const img = parseBitsJpeg2(tag.data);
if (img && !rawImages.has(img.id)) {
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
}
break;
}
case TAG_DEFINE_BITS_JPEG3: {
const img = parseBitsJpeg3(tag.data);
if (img && !rawImages.has(img.id)) {
rawImages.set(img.id, { width: img.width, height: img.height, pixels: img.pixels });
}
break;
}
}
}
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;
}
case TAG_DEFINE_BITS_LOSSLESS2: {
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;
}
case TAG_DEFINE_BITS_JPEG2: {
const img = parseBitsJpeg2(tag.data);
if (img && !rawImages.has(img.id)) {
rawImages.set(img.id, {
width: img.width,
height: img.height,
pixels: img.pixels,
});
}
break;
}
case TAG_DEFINE_BITS_JPEG3: {
const img = parseBitsJpeg3(tag.data);
if (img && !rawImages.has(img.id)) {
rawImages.set(img.id, {
width: img.width,
height: img.height,
pixels: img.pixels,
});
}
break;
}
}
}
// 5. Strip the SymbolClass package prefix
const prefix = `${classname}_`;
// 5. Strip the SymbolClass package prefix
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;
}
function stripPrefix(name: string): string {
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 = "";
// 6. Parse XML data from binary tags
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;
}
}
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;
}
}
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>();
if (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;
if (!asset.attrs.source) {
neededSprites.add(assetName);
} else {
neededSprites.add(asset.attrs.source);
}
}
}
}
// Build set of needed sprites from assets.xml
const neededSprites = new Set<string>();
if (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;
if (!asset.attrs.source) {
neededSprites.add(assetName);
} else {
neededSprites.add(asset.attrs.source);
}
}
}
}
// Map images to named sprites
const namedImages: SpriteImage[] = [];
for (const [id, imgData] of rawImages) {
const rawName = symbolMap.get(id);
if (!rawName) continue;
const name = stripPrefix(rawName);
// Map images to named sprites
const namedImages: SpriteImage[] = [];
for (const [id, imgData] of rawImages) {
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,
name,
width: imgData.width,
height: imgData.height,
pixels: imgData.pixels,
});
}
namedImages.push({
id,
name,
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);
// 7. Build spritesheet using bin-packing
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);
// 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);
// Build IMAGE_SOURCES map
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 (symbolName !== canonicalName) {
imageSources.set(stripPrefix(symbolName), stripPrefix(canonicalName));
}
}
// Build IMAGE_SOURCES map
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 (symbolName !== canonicalName) {
imageSources.set(stripPrefix(symbolName), stripPrefix(canonicalName));
}
}
// Build assets map
const assets: Record<
string,
{
x: number;
y: number;
source?: string;
flipH?: boolean;
flipV?: boolean;
usesPalette?: boolean;
}
> = {};
// Build assets map
const assets: Record<
string,
{
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 };
}
for (const ma of manifest.assets) {
assets[ma.name] = { x: ma.x, y: ma.y };
}
if (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;
if (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 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 || "";
if (source && imageSources.has(source)) {
source = imageSources.get(source)!;
}
if (imageSources.has(assetName)) {
source = imageSources.get(assetName)!;
}
let source = asset.attrs.source || "";
if (source && imageSources.has(source)) {
source = imageSources.get(source)!;
}
if (imageSources.has(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;
}
if (asset.attrs.flipV === "1") {
entry.flipV = true;
}
if (asset.attrs.usesPalette === "1") {
entry.usesPalette = true;
}
const entry: (typeof assets)[string] = source
? ({ source, x, y } as (typeof assets)[string])
: { x, y };
if (asset.attrs.flipH === "1") {
entry.flipH = true;
}
if (asset.attrs.flipV === "1") {
entry.flipV = 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 };
}
}
if (Object.keys(assets).length === 0) {
for (const img of namedImages) {
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];
}
}
for (const key of Object.keys(assets)) {
if (!validSpriteNames.has(key) && !assets[key].source) {
delete assets[key];
}
}
const sortedAssets: typeof assets = {};
for (const key of Object.keys(assets).sort()) {
sortedAssets[key] = assets[key];
}
const sortedAssets: typeof assets = {};
for (const key of Object.keys(assets).sort()) {
sortedAssets[key] = assets[key];
}
// Build spritesheet frames
const spritesheetFrames: Record<string, unknown> = {};
for (const img of namedImages) {
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,
trimmed: false,
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 },
};
}
// Build spritesheet frames
const spritesheetFrames: Record<string, unknown> = {};
for (const img of namedImages) {
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,
trimmed: false,
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";
// Determine visualization/logic types
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";
}
}
if (!indexData) {
if (
visualizations.some(
(v) => v.animations && Object.keys(v.animations).length > 0,
)
) {
visualizationType = "furniture_animated";
logicType = "furniture_multistate";
}
}
// 9. Build final JSON
const logicObj: Record<string, unknown> = {
model: {
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;
// 9. Build final JSON
const logicObj: Record<string, unknown> = {
model: {
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;
const nitroJson: Record<string, unknown> = {
name: classname,
visualizationType,
logicType,
spritesheet: {
meta: {
image: `${classname}.png`,
format: "RGBA8888",
size: { w: sheetWidth, h: sheetHeight },
scale: 1,
},
frames: spritesheetFrames,
},
assets: sortedAssets,
};
const nitroJson: Record<string, unknown> = {
name: classname,
visualizationType,
logicType,
spritesheet: {
meta: {
image: `${classname}.png`,
format: "RGBA8888",
size: { w: sheetWidth, h: sheetHeight },
scale: 1,
},
frames: spritesheetFrames,
},
assets: sortedAssets,
};
if (Object.keys(palettes).length > 0) {
nitroJson.palettes = palettes;
}
if (Object.keys(palettes).length > 0) {
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);
// 10. Create .nitro bundle
const bundle = createNitroBundle(nitroJson, png, classname);
return {
bundle,
classname,
dimensions: logic.dimensions,
directions: logic.directions,
spriteCount: namedImages.length,
warnings,
metadata: {
visualizationType,
logicType,
canstandon: logic.canstandon,
cansiton: logic.cansiton,
canlayon: logic.canlayon,
specialtype: indexData?.specialtype ?? 0,
defaultdir: computeDefaultDir(logic.directions),
partcolors: extractPartColors(visualizations),
},
};
return {
bundle,
classname,
dimensions: logic.dimensions,
directions: logic.directions,
spriteCount: namedImages.length,
warnings,
metadata: {
visualizationType,
logicType,
canstandon: logic.canstandon,
cansiton: logic.cansiton,
canlayon: logic.canlayon,
specialtype: indexData?.specialtype ?? 0,
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);
export function extractIconFromSwf(
swfBuffer: Buffer,
classname: string,
): Buffer | null {
try {
const decompressed = decompressSwf(swfBuffer);
const tags = parseSwfTags(decompressed);
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;
}
}
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 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;
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;
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;
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;
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);
} catch {
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);
} catch {
return null;
}
}
// ── Backward-compatible wrapper ────────────────────────────────────
export function convertSwfToNitroBuffer(swfBuffer: Buffer, classname: string): Buffer {
return convertSwfToNitro(swfBuffer, classname).bundle;
export function convertSwfToNitroBuffer(
swfBuffer: Buffer,
classname: string,
): Buffer {
return convertSwfToNitro(swfBuffer, classname).bundle;
}
// ── Batch Conversion ───────────────────────────────────────────────
export function convertSwfToNitroBatch(items: BatchConversionItem[]): BatchConversionResult[] {
return items.map((item) => {
try {
const result = convertSwfToNitro(item.swfBuffer, item.classname);
return {
classname: item.classname,
success: true,
bundle: result.bundle,
dimensions: result.dimensions,
directions: result.directions,
spriteCount: result.spriteCount,
warnings: result.warnings,
};
} catch (err) {
return {
classname: item.classname,
success: false,
warnings: [],
error: (err as Error).message,
};
}
});
export function convertSwfToNitroBatch(
items: BatchConversionItem[],
): BatchConversionResult[] {
return items.map((item) => {
try {
const result = convertSwfToNitro(item.swfBuffer, item.classname);
return {
classname: item.classname,
success: true,
bundle: result.bundle,
dimensions: result.dimensions,
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);
} catch {
try {
return gunzipSync(data);
} catch {
return data;
}
}
try {
return inflateSync(data);
} catch {
try {
return gunzipSync(data);
} catch {
return data;
}
}
}
+204 -179
View File
@@ -4,88 +4,88 @@ import type { SpriteImage } from "./image-decoder";
// ── Minimal PNG Encoder ────────────────────────────────────────────
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c;
}
return table;
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c;
}
return table;
})();
function crc32(data: Buffer): number {
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
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));
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 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 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]));
// Signature
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));
// 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));
// IDAT
chunks.push(pngChunk("IDAT", compressed));
// IEND
chunks.push(pngChunk("IEND", Buffer.alloc(0)));
// IEND
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;
}
/**
@@ -93,168 +93,193 @@ function nextPow2(v: number): number {
* Sorts sprites by height descending, then packs left-to-right in rows.
* Produces much tighter spritesheets than simple vertical stacking.
*/
function binPack(images: SpriteImage[]): { rects: PackedRect[]; width: number; height: number } {
if (images.length === 0) {
return { rects: [], width: 1, height: 1 };
}
function binPack(images: SpriteImage[]): {
rects: PackedRect[];
width: number;
height: number;
} {
if (images.length === 0) {
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);
// Sort by height descending for better shelf packing
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)));
// 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)));
// Round up to power of 2 for GPU friendliness (max 2048)
atlasWidth = Math.min(2048, nextPow2(atlasWidth));
// Round up to power of 2 for GPU friendliness (max 2048)
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;
}
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;
}
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 };
}
if (images.length === 0) {
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]));
// Build lookup by name for pixel data
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;
for (const rect of rects) {
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);
}
frames.set(rect.name, { x: rect.x, y: rect.y, w: rect.w, h: rect.h });
}
// 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);
}
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);
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 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);
const totalSize =
2 +
(2 + jsonFileName.length + 4 + jsonCompressed.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;
if (fileCount < 2) {
throw new Error(`Expected at least 2 files in .nitro bundle, got ${fileCount}`);
}
const fileCount = buffer.readInt16BE(offset);
offset += 2;
if (fileCount < 2) {
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);
// 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);
// 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);
// 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);
return { json, jsonFileName, png, pngFileName };
return { json, jsonFileName, png, pngFileName };
}
+85 -85
View File
@@ -17,136 +17,136 @@ 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>;
/** symbol name → charID (all symbols including aliases) */
symbols: Map<string, number>;
/** charID → first symbol name (canonical name) */
classNames: Map<number, string>;
/** symbol name → charID (all symbols including aliases) */
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;
while (offset < swfBuffer.length) {
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 (tagLength === 0x3f) {
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;
for (let i = 0; i < numSymbols; i++) {
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);
if (!classNames.has(charId)) {
classNames.set(charId, name);
}
}
symbols.set(name, charId);
if (!classNames.has(charId)) {
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 };
}
File diff suppressed because it is too large. Load diff