From eb7ef3753181d4cb232f17c9a3973a8aa7da7a52 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sun, 8 Feb 2026 12:14:25 -0600 Subject: [PATCH 1/4] Improve failure modes, fix DXT1 lookup in transparency mode # Conflicts: # src/map-parser.ts # src/parse-dxt.ts --- .gitignore | 2 + src/map-parser.ts | 262 +++++++++++++++++++++++++++++++++++++--------- src/parse-dxt.ts | 99 +++++++++--------- 3 files changed, 264 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 5ede78e..b8b5f1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ dist node_modules /working-files @@ -7,3 +8,4 @@ node_modules /texture.png /test.jpg /test.png +/test/output/ diff --git a/src/map-parser.ts b/src/map-parser.ts index 2028aad..140c35c 100644 --- a/src/map-parser.ts +++ b/src/map-parser.ts @@ -77,7 +77,9 @@ export class MapParser { const fileExt = filePath.ext; const tempArchiveDir = path.join(os.tmpdir(), fileName); - const sigintBinding = process.on("SIGINT", async () => this.sigint(tempArchiveDir)); + // register a named handler so we can remove only our listener later + const sigintHandler = async () => this.sigint(tempArchiveDir); + process.on("SIGINT", sigintHandler); try { if (fileExt !== ".sd7" && fileExt !== ".sdz") { @@ -123,7 +125,8 @@ export class MapParser { scriptName = archive.smfName; } - sigintBinding.removeAllListeners(); + // remove only our SIGINT listener + process.removeListener("SIGINT", sigintHandler); let resources: Record | undefined; if (this.config.parseResources) { @@ -150,7 +153,7 @@ export class MapParser { }; } catch (err) { await this.cleanup(tempArchiveDir); - sigintBinding.removeAllListeners(); + process.removeListener("SIGINT", sigintHandler); console.error(err); throw err; } @@ -262,36 +265,60 @@ export class MapParser { const level = percent * 255; return [level, level, level, 255]; }); - const heightMap = new Jimp({ - data: Buffer.from(heightMapColors.flat()), - width: mapWidth + 1, - height: mapHeight + 1 - }); + const hmBuf = Buffer.from(heightMapColors.flat()); + + let heightMap: Jimp; + try { + heightMap = new Jimp({ data: hmBuf, width: mapWidth + 1, height: mapHeight + 1 }); + } catch (err) { + const empty = Buffer.alloc((mapWidth + 1) * (mapHeight + 1) * 4, 0); + heightMap = new Jimp({ data: empty, width: mapWidth + 1, height: mapHeight + 1 }); + } const typeMapSize = (mapWidth/2) * (mapHeight/2); const typeMapBuffer = smfBuffer.slice(typeMapIndex, typeMapIndex + typeMapSize); - const typeMap = new Jimp({ - data: singleChannelToQuadChannel(typeMapBuffer), - width: mapWidth / 2, - height: mapHeight / 2 - }); + const tmBuf = singleChannelToQuadChannel(typeMapBuffer); + + let typeMap: Jimp; + try { + typeMap = new Jimp({ data: tmBuf, width: mapWidth / 2, height: mapHeight / 2 }); + } catch (err) { + const empty = Buffer.alloc((mapWidth / 2) * (mapHeight / 2) * 4, 0); + typeMap = new Jimp({ data: empty, width: mapWidth / 2, height: mapHeight / 2 }); + } + + // Calculate miniMap size from surrounding indices instead of hardcoding + let miniMapSize = 0; + if (metalMapIndex && metalMapIndex > miniMapIndex) { + miniMapSize = metalMapIndex - miniMapIndex; + } else if (featureMapIndex && featureMapIndex > miniMapIndex) { + miniMapSize = featureMapIndex - miniMapIndex; + } else { + miniMapSize = smfBuffer.length - miniMapIndex; + } - const miniMapSize = 699048; const miniMapBuffer = smfBuffer.slice(miniMapIndex, miniMapIndex + miniMapSize); const miniMapRgbaBuffer = parseDxt(miniMapBuffer, 1024, 1024); - const miniMap = new Jimp({ - data: miniMapRgbaBuffer, - width: 1024, - height: 1024 - }); + + let miniMap: Jimp; + try { + miniMap = new Jimp({ data: miniMapRgbaBuffer, width: 1024, height: 1024 }); + } catch (err) { + const empty = Buffer.alloc(1024 * 1024 * 4, 0); + miniMap = new Jimp({ data: empty, width: 1024, height: 1024 }); + } const metalMapSize = (mapWidth/2) * (mapHeight/2); const metalMapBuffer = smfBuffer.slice(metalMapIndex, metalMapIndex + metalMapSize); - const metalMap = new Jimp({ - data: singleChannelToQuadChannel(metalMapBuffer), - width: mapWidth / 2, - height: mapHeight / 2 - }); + const mmBuf = singleChannelToQuadChannel(metalMapBuffer); + + let metalMap: Jimp; + try { + metalMap = new Jimp({ data: mmBuf, width: mapWidth / 2, height: mapHeight / 2 }); + } catch (err) { + const empty = Buffer.alloc((mapWidth / 2) * (mapHeight / 2) * 4, 0); + metalMap = new Jimp({ data: empty, width: mapWidth / 2, height: mapHeight / 2 }); + } const tileIndexMapBufferStream = new BufferStream(smfBuffer.slice(tileIndexMapIndex)); const numOfTileFiles = tileIndexMapBufferStream.readInt(); @@ -328,27 +355,139 @@ export class MapParser { const tileSize = bufferStream.readInt(); const compressionType = bufferStream.readInt(); - const startIndex = mipmapSize === 32 ? 0 : mipmapSize === 16 ? 512 : mipmapSize === 8 ? 640 : 672; - const dxt1Size = Math.pow(mipmapSize, 2) / 2; - const rowLength = mipmapSize * 4; - - const refTiles: Buffer[][] = []; - for (let i=0; i 0 ? Math.floor(dataSize / numOfTiles) : 680; + + let TILE_STRIDE: number; + let bytesToRead: number; + let real_w = 4, real_h = 4; + + if (calcStride >= 512) { + TILE_STRIDE = 680; real_w = 32; real_h = 32; bytesToRead = 512; + } else { + TILE_STRIDE = calcStride; bytesToRead = calcStride; + if (calcStride >= 128) { real_w = 16; real_h = 16; } + else if (calcStride >= 32) { real_w = 8; real_h = 8; } + else { real_w = 4; real_h = 4; } + } + + const rowLength = real_w * 4; + + // We'll assemble tiles at the requested mipmapSize. When tiles are stored at larger sizes + // (e.g. 32x32) we'll decode at real_w/real_h then resize down to mipmapSize. If stored at + // smaller sizes we'll decode and scale up. + const assembledRowLength = mipmapSize * 4; + + // Prepare default empty tile to fill missing indices (mipmapSize rows) + const defaultRow = Buffer.alloc(assembledRowLength, 0); + const defaultTile: Buffer[] = []; + for (let r = 0; r < mipmapSize; r++) defaultTile.push(Buffer.from(defaultRow)); + + // pre-allocate refTiles with placeholders sized to mipmapSize + const refTiles: Buffer[][] = new Array(numOfTiles).fill(null).map(() => defaultTile.map(row => Buffer.from(row))); + + const uniqueIndices = Array.from(new Set(tileIndexes)); + const smtDataStart = headerSize; + let successCount = 0; + + for (const tileId of uniqueIndices) { + if (typeof tileId !== 'number') continue; + if (tileId < 0 || tileId >= numOfTiles) continue; + + const offset = smtDataStart + (tileId * TILE_STRIDE); + if (offset + bytesToRead > smtBuffer.length) continue; + + // Determine expected DXT length for this tile's native resolution + const dxtLen = (real_w * real_h) / 2; // bytes for DXT1 + + // If tiles are stored in 680-byte blocks with multiple mipmaps embedded, pick the correct offset + let dxtSlice: Buffer | null = null; + if (TILE_STRIDE === 680) { + const tileBlock = smtBuffer.slice(offset, Math.min(offset + TILE_STRIDE, smtBuffer.length)); + const startIndex = real_w === 32 ? 0 : real_w === 16 ? 512 : real_w === 8 ? 640 : 672; + dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); + } else { + // Tiles are tightly packed per-mip; read only the expected DXT length + if (offset + dxtLen > smtBuffer.length) continue; + dxtSlice = smtBuffer.slice(offset, offset + dxtLen); + } + + if (!dxtSlice || dxtSlice.length < dxtLen) continue; + + try { + // Decode the tile at its native resolution using the exact DXT bytes + const refTileRGBABuffer = parseDxt(dxtSlice, real_w, real_h); + + // Create a temporary Jimp image to perform a nearest-neighbour resize to the requested mipmapSize + let tileImage = new Jimp({ data: Buffer.from(refTileRGBABuffer), width: real_w, height: real_h }); + if (real_w !== mipmapSize || real_h !== mipmapSize) { + tileImage = tileImage.resize(mipmapSize, mipmapSize, Jimp.RESIZE_NEAREST_NEIGHBOR); + } + + // Extract per-row buffers at the assembled mip size + const assembledRows: Buffer[] = []; + const tileData = tileImage.bitmap.data; + for (let k = 0; k < mipmapSize; k++) { + const pixelIndex = k * mipmapSize * 4; + const rowBuf = Buffer.from(tileData.slice(pixelIndex, pixelIndex + mipmapSize * 4)); + assembledRows.push(rowBuf); + } + refTiles[tileId] = assembledRows; + successCount++; + } catch (err) { + // ignore single tile failures } - refTiles.push(refTile); + } + + if (successCount === 0) { + // fallback: try to decode sequentially like older implementation + bufferStream.destroy(); + for (let i=0; i smtBuffer.length) break; + try { + const dxtLen = (real_w * real_h) / 2; + let dxtSlice: Buffer | null = null; + if (TILE_STRIDE === 680) { + const tileBlock = smtBuffer.slice(offset, Math.min(offset + TILE_STRIDE, smtBuffer.length)); + const startIndex = real_w === 32 ? 0 : real_w === 16 ? 512 : real_w === 8 ? 640 : 672; + dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); + } else { + if (offset + dxtLen > smtBuffer.length) continue; + dxtSlice = smtBuffer.slice(offset, offset + dxtLen); + } + + if (!dxtSlice || dxtSlice.length < dxtLen) continue; + + const refTileRGBABuffer = parseDxt(dxtSlice, real_w, real_h); + + let tileImage = new Jimp({ data: Buffer.from(refTileRGBABuffer), width: real_w, height: real_h }); + if (real_w !== mipmapSize || real_h !== mipmapSize) { + tileImage = tileImage.resize(mipmapSize, mipmapSize, Jimp.RESIZE_NEAREST_NEIGHBOR); + } + + const assembledRows: Buffer[] = []; + const tileData = tileImage.bitmap.data; + for (let k = 0; k < mipmapSize; k++) { + const pixelIndex = k * mipmapSize * 4; + const rowBuf = Buffer.from(tileData.slice(pixelIndex, pixelIndex + mipmapSize * 4)); + assembledRows.push(rowBuf); + } + refTiles[i] = assembledRows; + } catch (err) { + // ignore + } + } + } else { + bufferStream.destroy(); } const tiles: Buffer[][] = []; for (let i=0; i { @@ -396,7 +550,7 @@ export class MapParser { if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") { obj[field.key.name] = field.value.value; } else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") { - obj[field.key.name] = -field.value.argument.value; + obj[field.key.name] = -((field.value.argument as any).value); } else if (field.value.type === "TableConstructorExpression") { obj[field.key.name] = this.parseMapInfoFields(field.value.fields); } @@ -408,10 +562,14 @@ export class MapParser { } else if (field.type === "TableKey") { if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") { if (field.key.type === "NumericLiteral") { - arr[field.key.type] = field.value.value; + // use the numeric literal value as the array index (was using .type previously which is incorrect) + arr[(field.key as any).value] = field.value.value; } } else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") { - arr[field.key.type] = -field.value.argument.value; + // Ensure the key is a numeric literal before using .value, and assert types for the unary argument + if (field.key.type === "NumericLiteral") { + arr[(field.key as any).value] = -((field.value.argument as any).value); + } } else if (field.value.type === "TableConstructorExpression") { arr.push(this.parseMapInfoFields(field.value.fields)); } @@ -475,9 +633,9 @@ export class MapParser { return clone; } - protected joinTilesHorizontally(tiles: Buffer[][], mipmapSize: 4 | 8 | 16 | 32) : Buffer { + protected joinTilesHorizontally(tiles: Buffer[][], rows: number) : Buffer { const tileRows: Buffer[] = []; - for (let y=0; y colorValue1) { // Non transparent mode - lookup[0] = Math.floor((color0.R) * 255); - lookup[1] = Math.floor((color0.G) * 255); - lookup[2] = Math.floor((color0.B) * 255); - lookup[3] = Math.floor(255); - - lookup[4] = Math.floor((color1.R) * 255); - lookup[5] = Math.floor((color1.G) * 255); - lookup[6] = Math.floor((color1.B) * 255); - lookup[7] = Math.floor(255); - - lookup[8] = Math.floor((color0.R * 2 / 3 + color1.R * 1 / 3) * 255); - lookup[9] = Math.floor((color0.G * 2 / 3 + color1.G * 1 / 3) * 255); - lookup[10] = Math.floor((color0.B * 2 / 3 + color1.B * 1 / 3) * 255); - lookup[11] = Math.floor(255); - - lookup[12] = Math.floor((color0.R * 1 / 3 + color1.R * 2 / 3) * 255); - lookup[13] = Math.floor((color0.G * 1 / 3 + color1.G * 2 / 3) * 255); - lookup[14] = Math.floor((color0.B * 1 / 3 + color1.B * 2 / 3) * 255); - lookup[15] = Math.floor(255); + lookup[0] = color0.R; + lookup[1] = color0.G; + lookup[2] = color0.B; + lookup[3] = 255; + + lookup[4] = color1.R; + lookup[5] = color1.G; + lookup[6] = color1.B; + lookup[7] = 255; + + lookup[8] = Math.floor((color0.R * 2 + color1.R * 1) / 3); + lookup[9] = Math.floor((color0.G * 2 + color1.G * 1) / 3); + lookup[10] = Math.floor((color0.B * 2 + color1.B * 1) / 3); + lookup[11] = 255; + + lookup[12] = Math.floor((color0.R * 1 + color1.R * 2) / 3); + lookup[13] = Math.floor((color0.G * 1 + color1.G * 2) / 3); + lookup[14] = Math.floor((color0.B * 1 + color1.B * 2) / 3); + lookup[15] = 255; } else { // transparent mode - lookup[0] = Math.floor((color0.R) * 255); - lookup[1] = Math.floor((color0.G) * 255); - lookup[2] = Math.floor((color0.B) * 255); - lookup[3] = Math.floor(255); - - lookup[4] = Math.floor((color0.R * 1 / 2 + color1.R * 1 / 2) * 255); - lookup[5] = Math.floor((color0.G * 1 / 2 + color1.G * 1 / 2) * 255); - lookup[6] = Math.floor((color0.B * 1 / 2 + color1.B * 1 / 2) * 255); - lookup[7] = Math.floor(255); - - lookup[8] = Math.floor((color1.R) * 255); - lookup[9] = Math.floor((color1.G) * 255); - lookup[10] = Math.floor((color1.B) * 255); - lookup[11] = Math.floor(255); - - lookup[12] = Math.floor(0); - lookup[13] = Math.floor(0); - lookup[14] = Math.floor(0); - lookup[15] = Math.floor(0); + lookup[0] = color0.R; + lookup[1] = color0.G; + lookup[2] = color0.B; + lookup[3] = 255; + + lookup[4] = color1.R; + lookup[5] = color1.G; + lookup[6] = color1.B; + lookup[7] = 255; + + lookup[8] = Math.floor((color0.R + color1.R) / 2); + lookup[9] = Math.floor((color0.G + color1.G) / 2); + lookup[10] = Math.floor((color0.B + color1.B) / 2); + lookup[11] = 255; + + lookup[12] = 0; + lookup[13] = 0; + lookup[14] = 0; + lookup[15] = 0; } return lookup; @@ -137,13 +138,15 @@ function generateDXT1Lookup(colorValue0: number, colorValue1: number, out = null function getComponentsFromRGB565(color: number) { - return { - R: ((color & 0b11111000_00000000) >> 8) / 0xff, - G: ((color & 0b00000111_11100000) >> 3) / 0xff, - B: ((color & 0b00000000_00011111) << 3) / 0xff - }; + // Simple bit shift approach matching Python implementation + // This produces smoother gradients with fewer artifacts than bit replication + const r = (color & 0xF800) >> 8; // 5 bits shifted to positions 3-7 + const g = (color & 0x07E0) >> 3; // 6 bits shifted to positions 2-7 + const b = (color & 0x001F) << 3; // 5 bits shifted to positions 3-7 + + return { R: r, G: g, B: b }; } function makeRGB565(r: number, g: number, b: number) { return ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | ((b & 0b11111000) >> 3); -} \ No newline at end of file +} From d635316732d32e44a97df7fb661496237550d37a Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sun, 8 Feb 2026 12:41:10 -0600 Subject: [PATCH 2/4] Fix linting errors --- src/map-parser.ts | 52 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/map-parser.ts b/src/map-parser.ts index 140c35c..8b4bf39 100644 --- a/src/map-parser.ts +++ b/src/map-parser.ts @@ -368,9 +368,13 @@ export class MapParser { TILE_STRIDE = 680; real_w = 32; real_h = 32; bytesToRead = 512; } else { TILE_STRIDE = calcStride; bytesToRead = calcStride; - if (calcStride >= 128) { real_w = 16; real_h = 16; } - else if (calcStride >= 32) { real_w = 8; real_h = 8; } - else { real_w = 4; real_h = 4; } + if (calcStride >= 128) { + real_w = 16; real_h = 16; + } else if (calcStride >= 32) { + real_w = 8; real_h = 8; + } else { + real_w = 4; real_h = 4; + } } const rowLength = real_w * 4; @@ -383,7 +387,9 @@ export class MapParser { // Prepare default empty tile to fill missing indices (mipmapSize rows) const defaultRow = Buffer.alloc(assembledRowLength, 0); const defaultTile: Buffer[] = []; - for (let r = 0; r < mipmapSize; r++) defaultTile.push(Buffer.from(defaultRow)); + for (let r = 0; r < mipmapSize; r++) { + defaultTile.push(Buffer.from(defaultRow)); + } // pre-allocate refTiles with placeholders sized to mipmapSize const refTiles: Buffer[][] = new Array(numOfTiles).fill(null).map(() => defaultTile.map(row => Buffer.from(row))); @@ -393,11 +399,17 @@ export class MapParser { let successCount = 0; for (const tileId of uniqueIndices) { - if (typeof tileId !== 'number') continue; - if (tileId < 0 || tileId >= numOfTiles) continue; + if (typeof tileId !== "number") { + continue; + } + if (tileId < 0 || tileId >= numOfTiles) { + continue; + } const offset = smtDataStart + (tileId * TILE_STRIDE); - if (offset + bytesToRead > smtBuffer.length) continue; + if (offset + bytesToRead > smtBuffer.length) { + continue; + } // Determine expected DXT length for this tile's native resolution const dxtLen = (real_w * real_h) / 2; // bytes for DXT1 @@ -410,11 +422,15 @@ export class MapParser { dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); } else { // Tiles are tightly packed per-mip; read only the expected DXT length - if (offset + dxtLen > smtBuffer.length) continue; + if (offset + dxtLen > smtBuffer.length) { + continue; + } dxtSlice = smtBuffer.slice(offset, offset + dxtLen); } - if (!dxtSlice || dxtSlice.length < dxtLen) continue; + if (!dxtSlice || dxtSlice.length < dxtLen) { + continue; + } try { // Decode the tile at its native resolution using the exact DXT bytes @@ -446,7 +462,9 @@ export class MapParser { bufferStream.destroy(); for (let i=0; i smtBuffer.length) break; + if (offset + bytesToRead > smtBuffer.length) { + break; + } try { const dxtLen = (real_w * real_h) / 2; let dxtSlice: Buffer | null = null; @@ -455,11 +473,15 @@ export class MapParser { const startIndex = real_w === 32 ? 0 : real_w === 16 ? 512 : real_w === 8 ? 640 : 672; dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); } else { - if (offset + dxtLen > smtBuffer.length) continue; + if (offset + dxtLen > smtBuffer.length) { + continue; + } dxtSlice = smtBuffer.slice(offset, offset + dxtLen); } - if (!dxtSlice || dxtSlice.length < dxtLen) continue; + if (!dxtSlice || dxtSlice.length < dxtLen) { + continue; + } const refTileRGBABuffer = parseDxt(dxtSlice, real_w, real_h); @@ -550,7 +572,7 @@ export class MapParser { if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") { obj[field.key.name] = field.value.value; } else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") { - obj[field.key.name] = -((field.value.argument as any).value); + obj[field.key.name] = -field.value.argument.value; } else if (field.value.type === "TableConstructorExpression") { obj[field.key.name] = this.parseMapInfoFields(field.value.fields); } @@ -563,12 +585,12 @@ export class MapParser { if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") { if (field.key.type === "NumericLiteral") { // use the numeric literal value as the array index (was using .type previously which is incorrect) - arr[(field.key as any).value] = field.value.value; + arr[field.key.value] = field.value.value; } } else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") { // Ensure the key is a numeric literal before using .value, and assert types for the unary argument if (field.key.type === "NumericLiteral") { - arr[(field.key as any).value] = -((field.value.argument as any).value); + arr[field.key.value] = -field.value.argument.value; } } else if (field.value.type === "TableConstructorExpression") { arr.push(this.parseMapInfoFields(field.value.fields)); From 4f665d94e883a193c7d9a54f6b7f08e0f263ac66 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Tue, 10 Feb 2026 21:22:15 -0600 Subject: [PATCH 3/4] Remove all unnecessary changes from map-parser --- src/map-parser.ts | 271 ++++++++-------------------------------------- 1 file changed, 46 insertions(+), 225 deletions(-) diff --git a/src/map-parser.ts b/src/map-parser.ts index f4d3473..fbd98a7 100644 --- a/src/map-parser.ts +++ b/src/map-parser.ts @@ -82,9 +82,7 @@ export class MapParser { const fileExt = filePath.ext; const tempArchiveDir = path.join(os.tmpdir(), fileName); - // register a named handler so we can remove only our listener later - const sigintHandler = async () => this.sigint(tempArchiveDir); - process.on("SIGINT", sigintHandler); + const sigintBinding = process.on("SIGINT", async () => this.sigint(tempArchiveDir)); try { if (fileExt !== ".sd7" && fileExt !== ".sdz") { @@ -130,8 +128,7 @@ export class MapParser { scriptName = archive.smfName; } - // remove only our SIGINT listener - process.removeListener("SIGINT", sigintHandler); + sigintBinding.removeAllListeners(); let resources: Record | undefined; if (this.config.parseResources) { @@ -158,7 +155,7 @@ export class MapParser { }; } catch (err) { await this.cleanup(tempArchiveDir); - process.removeListener("SIGINT", sigintHandler); + sigintBinding.removeAllListeners(); console.error(err); throw err; } @@ -270,60 +267,36 @@ export class MapParser { const level = percent * 255; return [level, level, level, 255]; }); - const hmBuf = Buffer.from(heightMapColors.flat()); - - let heightMap: Jimp; - try { - heightMap = new Jimp({ data: hmBuf, width: mapWidth + 1, height: mapHeight + 1 }); - } catch (err) { - const empty = Buffer.alloc((mapWidth + 1) * (mapHeight + 1) * 4, 0); - heightMap = new Jimp({ data: empty, width: mapWidth + 1, height: mapHeight + 1 }); - } + const heightMap = new Jimp({ + data: Buffer.from(heightMapColors.flat()), + width: mapWidth + 1, + height: mapHeight + 1 + }); const typeMapSize = (mapWidth/2) * (mapHeight/2); const typeMapBuffer = smfBuffer.slice(typeMapIndex, typeMapIndex + typeMapSize); - const tmBuf = singleChannelToQuadChannel(typeMapBuffer); - - let typeMap: Jimp; - try { - typeMap = new Jimp({ data: tmBuf, width: mapWidth / 2, height: mapHeight / 2 }); - } catch (err) { - const empty = Buffer.alloc((mapWidth / 2) * (mapHeight / 2) * 4, 0); - typeMap = new Jimp({ data: empty, width: mapWidth / 2, height: mapHeight / 2 }); - } - - // Calculate miniMap size from surrounding indices instead of hardcoding - let miniMapSize = 0; - if (metalMapIndex && metalMapIndex > miniMapIndex) { - miniMapSize = metalMapIndex - miniMapIndex; - } else if (featureMapIndex && featureMapIndex > miniMapIndex) { - miniMapSize = featureMapIndex - miniMapIndex; - } else { - miniMapSize = smfBuffer.length - miniMapIndex; - } + const typeMap = new Jimp({ + data: singleChannelToQuadChannel(typeMapBuffer), + width: mapWidth / 2, + height: mapHeight / 2 + }); + const miniMapSize = 699048; const miniMapBuffer = smfBuffer.slice(miniMapIndex, miniMapIndex + miniMapSize); const miniMapRgbaBuffer = parseDxt(miniMapBuffer, 1024, 1024); - - let miniMap: Jimp; - try { - miniMap = new Jimp({ data: miniMapRgbaBuffer, width: 1024, height: 1024 }); - } catch (err) { - const empty = Buffer.alloc(1024 * 1024 * 4, 0); - miniMap = new Jimp({ data: empty, width: 1024, height: 1024 }); - } + const miniMap = new Jimp({ + data: miniMapRgbaBuffer, + width: 1024, + height: 1024 + }); const metalMapSize = (mapWidth/2) * (mapHeight/2); const metalMapBuffer = smfBuffer.slice(metalMapIndex, metalMapIndex + metalMapSize); - const mmBuf = singleChannelToQuadChannel(metalMapBuffer); - - let metalMap: Jimp; - try { - metalMap = new Jimp({ data: mmBuf, width: mapWidth / 2, height: mapHeight / 2 }); - } catch (err) { - const empty = Buffer.alloc((mapWidth / 2) * (mapHeight / 2) * 4, 0); - metalMap = new Jimp({ data: empty, width: mapWidth / 2, height: mapHeight / 2 }); - } + const metalMap = new Jimp({ + data: singleChannelToQuadChannel(metalMapBuffer), + width: mapWidth / 2, + height: mapHeight / 2 + }); const tileIndexMapBufferStream = new BufferStream(smfBuffer.slice(tileIndexMapIndex)); const numOfTileFiles = tileIndexMapBufferStream.readInt(); @@ -360,161 +333,27 @@ export class MapParser { const tileSize = bufferStream.readInt(); const compressionType = bufferStream.readInt(); - // compute header size (we've read 16 + 4*4 = 32 bytes) - const headerSize = bufferStream.getPosition(); - const dataSize = smtBuffer.length - headerSize; - const calcStride = numOfTiles > 0 ? Math.floor(dataSize / numOfTiles) : 680; - - let TILE_STRIDE: number; - let bytesToRead: number; - let real_w = 4, real_h = 4; - - if (calcStride >= 512) { - TILE_STRIDE = 680; real_w = 32; real_h = 32; bytesToRead = 512; - } else { - TILE_STRIDE = calcStride; bytesToRead = calcStride; - if (calcStride >= 128) { - real_w = 16; real_h = 16; - } else if (calcStride >= 32) { - real_w = 8; real_h = 8; - } else { - real_w = 4; real_h = 4; + const startIndex = mipmapSize === 32 ? 0 : mipmapSize === 16 ? 512 : mipmapSize === 8 ? 640 : 672; + const dxt1Size = Math.pow(mipmapSize, 2) / 2; + const rowLength = mipmapSize * 4; + + const refTiles: Buffer[][] = []; + for (let i=0; i defaultTile.map(row => Buffer.from(row))); - - const uniqueIndices = Array.from(new Set(tileIndexes)); - const smtDataStart = headerSize; - let successCount = 0; - - for (const tileId of uniqueIndices) { - if (typeof tileId !== "number") { - continue; - } - if (tileId < 0 || tileId >= numOfTiles) { - continue; - } - - const offset = smtDataStart + (tileId * TILE_STRIDE); - if (offset + bytesToRead > smtBuffer.length) { - continue; - } - - // Determine expected DXT length for this tile's native resolution - const dxtLen = (real_w * real_h) / 2; // bytes for DXT1 - - // If tiles are stored in 680-byte blocks with multiple mipmaps embedded, pick the correct offset - let dxtSlice: Buffer | null = null; - if (TILE_STRIDE === 680) { - const tileBlock = smtBuffer.slice(offset, Math.min(offset + TILE_STRIDE, smtBuffer.length)); - const startIndex = real_w === 32 ? 0 : real_w === 16 ? 512 : real_w === 8 ? 640 : 672; - dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); - } else { - // Tiles are tightly packed per-mip; read only the expected DXT length - if (offset + dxtLen > smtBuffer.length) { - continue; - } - dxtSlice = smtBuffer.slice(offset, offset + dxtLen); - } - - if (!dxtSlice || dxtSlice.length < dxtLen) { - continue; - } - - try { - // Decode the tile at its native resolution using the exact DXT bytes - const refTileRGBABuffer = parseDxt(dxtSlice, real_w, real_h); - - // Create a temporary Jimp image to perform a nearest-neighbour resize to the requested mipmapSize - let tileImage = new Jimp({ data: Buffer.from(refTileRGBABuffer), width: real_w, height: real_h }); - if (real_w !== mipmapSize || real_h !== mipmapSize) { - tileImage = tileImage.resize(mipmapSize, mipmapSize, Jimp.RESIZE_NEAREST_NEIGHBOR); - } - - // Extract per-row buffers at the assembled mip size - const assembledRows: Buffer[] = []; - const tileData = tileImage.bitmap.data; - for (let k = 0; k < mipmapSize; k++) { - const pixelIndex = k * mipmapSize * 4; - const rowBuf = Buffer.from(tileData.slice(pixelIndex, pixelIndex + mipmapSize * 4)); - assembledRows.push(rowBuf); - } - refTiles[tileId] = assembledRows; - successCount++; - } catch (err) { - // ignore single tile failures - } - } - - if (successCount === 0) { - // fallback: try to decode sequentially like older implementation - bufferStream.destroy(); - for (let i=0; i smtBuffer.length) { - break; - } - try { - const dxtLen = (real_w * real_h) / 2; - let dxtSlice: Buffer | null = null; - if (TILE_STRIDE === 680) { - const tileBlock = smtBuffer.slice(offset, Math.min(offset + TILE_STRIDE, smtBuffer.length)); - const startIndex = real_w === 32 ? 0 : real_w === 16 ? 512 : real_w === 8 ? 640 : 672; - dxtSlice = tileBlock.slice(startIndex, startIndex + dxtLen); - } else { - if (offset + dxtLen > smtBuffer.length) { - continue; - } - dxtSlice = smtBuffer.slice(offset, offset + dxtLen); - } - - if (!dxtSlice || dxtSlice.length < dxtLen) { - continue; - } - - const refTileRGBABuffer = parseDxt(dxtSlice, real_w, real_h); - - let tileImage = new Jimp({ data: Buffer.from(refTileRGBABuffer), width: real_w, height: real_h }); - if (real_w !== mipmapSize || real_h !== mipmapSize) { - tileImage = tileImage.resize(mipmapSize, mipmapSize, Jimp.RESIZE_NEAREST_NEIGHBOR); - } - - const assembledRows: Buffer[] = []; - const tileData = tileImage.bitmap.data; - for (let k = 0; k < mipmapSize; k++) { - const pixelIndex = k * mipmapSize * 4; - const rowBuf = Buffer.from(tileData.slice(pixelIndex, pixelIndex + mipmapSize * 4)); - assembledRows.push(rowBuf); - } - refTiles[i] = assembledRows; - } catch (err) { - // ignore - } - } - } else { - bufferStream.destroy(); + refTiles.push(refTile); } const tiles: Buffer[][] = []; for (let i=0; i { @@ -593,7 +417,6 @@ export class MapParser { arr[field.key.value] = field.value.value; } } else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") { - // Ensure the key is a numeric literal before using .value, and assert types for the unary argument if (field.key.type === "NumericLiteral") { arr[field.key.value] = -field.value.argument.value; } @@ -660,9 +483,9 @@ export class MapParser { return clone; } - protected joinTilesHorizontally(tiles: Buffer[][], rows: number) : Buffer { + protected joinTilesHorizontally(tiles: Buffer[][], mipmapSize: 4 | 8 | 16 | 32) : Buffer { const tileRows: Buffer[] = []; - for (let y=0; y Date: Tue, 10 Feb 2026 21:22:46 -0600 Subject: [PATCH 4/4] Allow other test maps to be kept locally for testing --- test/test_maps/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/test_maps/.gitignore diff --git a/test/test_maps/.gitignore b/test/test_maps/.gitignore new file mode 100644 index 0000000..5afe2be --- /dev/null +++ b/test/test_maps/.gitignore @@ -0,0 +1,3 @@ +* +!barren_2.sd7 +!tropical-v2.sdz