From 42ea54550724899f49854c9f6637a3184a8998cc Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Fri, 1 May 2026 14:57:11 +0200 Subject: [PATCH] add minify flag to build script (cherry picked from commit f0b43f7b7c8e6c7960000e57320656e5b21f3171) --- build.ts | 316 +++++++++++++++++++++++++++++++++++++----------------- readme.md | 5 +- 2 files changed, 221 insertions(+), 100 deletions(-) diff --git a/build.ts b/build.ts index cf8491b..01a50ca 100755 --- a/build.ts +++ b/build.ts @@ -1,14 +1,14 @@ #!/usr/bin/env -S deno run --allow-read -if (Deno.args.length !== 0) { - console.error( - "This script takes no argument (The source file list is hardcoded) and output the result on stdout.", - ); - Deno.exit(1); -} - import { TextLineStream } from "jsr:@std/streams@1.1.0"; +/** + * CONFIG + */ + +const instructionLength = 4; // in bytes +const rawDataInstructionLengthPrefix = "U32"; + const asmFilePaths = [ "src/consts/arch.asm", "src/consts/game.asm", @@ -25,136 +25,212 @@ const asmFilePaths = [ "src/lib/drawing.asm", "src/lib/sprite_rotation.asm", ]; -const reservedSpacePath = 'src/reserved_space.asm'; +const reservedSpacePath = "src/reserved_space.asm"; -const instructionLength = 4; // in bytes -const mapWidth = 15; -const spriteWidth = 5; - -const mapMatchingStrings = { - 0x00_00_00: '0', // Empty - 0xFF_00_00: '1', // Wall - 0xFF_FF_00: '2', // Coin +const mapMatchingValues = { + 0x00_00_00: 0, // Empty + 0xFF_00_00: 1, // Wall + 0xFF_FF_00: 2, // Coin }; -const spritesMatchingStrings = { - 0x00_00_00: ' 0', // 0b000_000_00 - 0x00_00_FF: ' 3', // 0b000_000_11 - 0x00_FF_00: ' 28', // 0b000_111_00 - 0x99_50_00: ' 68', // 0b010_001_00 - 0x77_77_77: '110', // 0b011_011_10 - 0xB3_B3_B3: '146', // 0b100_100_10 - 0xFF_00_00: '224', // 0b111_000_00 - 0xFF_00_FF: '227', // 0b111_000_11 - 0xFF_80_00: '240', // 0b111_100_00 - 0xFF_FF_00: '248', // 0b111_110_00 - 0xFF_FF_FF: '255', // 0b111_111_11 +const spritesMatchingValues = { + 0x00_00_00: 0, // 0b000_000_00 + 0x00_00_FF: 3, // 0b000_000_11 + 0x00_FF_00: 28, // 0b000_111_00 + 0x99_50_00: 68, // 0b010_001_00 + 0x77_77_77: 110, // 0b011_011_10 + 0xB3_B3_B3: 146, // 0b100_100_10 + 0xFF_00_00: 224, // 0b111_000_00 + 0xFF_00_FF: 227, // 0b111_000_11 + 0xFF_80_00: 240, // 0b111_100_00 + 0xFF_FF_00: 248, // 0b111_110_00 + 0xFF_FF_FF: 255, // 0b111_111_11 }; -const mapPath = 'assets/map.ppm'; +const mapPath = "assets/map.ppm"; const spritesPath = { - 'sprite_empty': 'assets/sprites/empty.ppm', - 'sprite_unimplemented': 'assets/sprites/placeholder.ppm', - 'sprite_wall_end': 'assets/sprites/wall_end.ppm', - 'sprite_wall_straight': 'assets/sprites/wall_straight.ppm', - 'sprite_wall_corner': 'assets/sprites/wall_corner.ppm', - 'sprite_robot': 'assets/sprites/robot.ppm', - 'sprite_ghost': 'assets/sprites/ghost.ppm', - 'sprite_coin': 'assets/sprites/coin.ppm', + "sprite_empty": "assets/sprites/empty.ppm", + "sprite_unimplemented": "assets/sprites/placeholder.ppm", + "sprite_wall_end": "assets/sprites/wall_end.ppm", + "sprite_wall_straight": "assets/sprites/wall_straight.ppm", + "sprite_wall_corner": "assets/sprites/wall_corner.ppm", + "sprite_robot": "assets/sprites/robot.ppm", + "sprite_ghost": "assets/sprites/ghost.ppm", + "sprite_coin": "assets/sprites/coin.ppm", }; /** - * START OF BUILD + * END OF CONFIG */ -for (const path of asmFilePaths) { - const file = await Deno.open(path); - const readable = file.readable.pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); +class Minifier { + private buffer = ""; + private lines: Array = []; + stream; - console.log(` + constructor(writableStream: WritableStream) { + this.stream = new TransformStream({ + transform: (chunk, controller) => { + this.buffer += chunk; + controller.enqueue(this.processChunk()); + }, + + flush: (controller) => { + this.buffer += "\n"; + controller.enqueue(this.processChunk()); + }, + }); + + this.stream.readable.pipeTo(writableStream); + } + + private processChunk(): string { + const newLines = this.buffer.split("\n"); + this.buffer = newLines.pop() as string; // keep the last (potentially incomplete) line + + const removeComment = (line: string) => line.replace(/\s*;.*/, ""); + const removeLeadingWhitespaces = (line: string) => line.replace(/^\s+/, ""); + const removeTrailingWhitespaces = (line: string) => + line.replace(/\s+$/, ""); + const removeDoubleSpaces = (line: string) => line.replaceAll(/ +/g, " "); + const isNotEmpty = (line: string) => line.length !== 0; + + const newLinesStripped = newLines + .map(removeComment) + .map(removeLeadingWhitespaces) + .map(removeTrailingWhitespaces) + .map(removeDoubleSpaces) + .filter(isNotEmpty); + + this.lines.push(...newLinesStripped); + + return this.processLines(); + } + + private processLines() { + const processedLines: Array = []; + while (this.lines.length >= instructionLength) { + const mergeableLines = this.lines.slice(0, instructionLength); + if (!mergeableLines.every((line) => line.startsWith("U8"))) { + processedLines.push(this.lines.shift() as string); + continue; + } + + const bytesToMerge = mergeableLines.map((line) => + parseInt((line.match(/U8\s(\d+)/) as RegExpMatchArray)[1]) + ); + let mergedBytes = 0; + for (let i = 0; i < bytesToMerge.length; i++) { + mergedBytes += bytesToMerge[i] << (instructionLength - 1 - i) * 8; + } + processedLines.push( + `${rawDataInstructionLengthPrefix} ${mergedBytes >>> 0}`, + ); + this.lines.splice(0, instructionLength); + } + + return processedLines.length === 0 ? "" : `${processedLines.join("\n")}\n`; + } +} + +async function build(output: WritableStreamDefaultWriter) { + for (const path of asmFilePaths) { + const file = await Deno.open(path); + const readable = file.readable.pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + + output.write(` ;/******************************* ;* ${path} ;*******************************/ `); - for await (const line of readable) { - if (!line.startsWith("include ")) { - console.log(line); + for await (const line of readable) { + if (!line.startsWith("include ")) { + output.write(`${line}\n`); + } } } -} + const asmRawRepresentations = []; -const asmRawRepresentations = []; + const mapBytes = await getDataFromPPM(mapPath); + asmRawRepresentations.push( + bytesToAsmConstU8("map", mapBytes, mapMatchingValues), + ); -const mapBytes = await getDataFromPPM(mapPath); -asmRawRepresentations.push(bytesToAsmConstU8('map', mapBytes, mapMatchingStrings, mapWidth)); + for (const [label, path] of Object.entries(spritesPath)) { + const bytes = await getDataFromPPM(path); + asmRawRepresentations.push( + bytesToAsmConstU8(label, bytes, spritesMatchingValues), + ); + } -for (const [label, path] of Object.entries(spritesPath)) { - const bytes = await getDataFromPPM(path); - asmRawRepresentations.push(bytesToAsmConstU8(label, bytes, spritesMatchingStrings, spriteWidth)); -} + output.write(asmRawRepresentations.join("\n\n")); -console.log(asmRawRepresentations.join("\n\n")); - -console.log(` + output.write(` ; ; RESERVED RAM SPACE ; `); -console.log(await Deno.readTextFile(reservedSpacePath)) + output.write(await Deno.readTextFile(reservedSpacePath)); +} /** * Return an ASM constant (U8) representation of the given data - * + * * If needed, the representation will be padded to ensure the following code stays aligned on the instruction's length - * + * * @param label The label to write before the first raw data * @param data An array of bytes to interprete as 24 bits values - * @param matchingStrings An associative array with a string representation for each possible 24 bit data value - * @param lineLength Optional: Provide a way to wrap the result in multiple lines - * + * @param matchingValues An associative array with a string representation for each possible 24 bit data value + * * @returns The ASM representation of that raw data, pre- and post-fixed by the given label */ -function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { - lineLength ??= Infinity; +function bytesToAsmConstU8( + label: string, + data: Uint8Array, + matchingValues: Record, +): string { + if (data.length % 3 !== 0) { + console.error( + "Length of data passed to bytesToAsmConstU8 are not a multiple of 3 (it needs to, as each resulting byte need a Red, a Green and a Blue value)", + ); + Deno.exit(1); + } - const stringValues = []; - for (let i = 0; i < data.length - 2; i += 3) { - const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) - if (!Object.hasOwn(matchingStrings, pixelValue)) { - console.error(`No matching value defined for "${pixelValue.toString(2)}" (which is ${label}'s ${i} pixel)`); + const lines = [`${label}:`]; + for (let i = 0; i < data.length; i += 3) { + const pixelValue = (data[i] << 16) + (data[i + 1] << 8) + (data[i + 2]); + if (!Object.hasOwn(matchingValues, pixelValue)) { + console.error( + `No matching value defined for "${ + pixelValue.toString(2) + }" (which is ${label}'s ${i} pixel)`, + ); Deno.exit(1); } - - stringValues.push(`U8 ${matchingStrings[pixelValue]}`); + + lines.push(`U8 ${matchingValues[pixelValue]}`); + } + lines.push(`${label}_end:`); + + const bytesNb = data.length / 3; + const unalignedBytesNb = bytesNb % instructionLength; + + for (let i = 0; i < instructionLength - unalignedBytesNb; i++) { + lines.push( + `U8 0 ; padding to preserve ${8 * instructionLength} bits alignment`, + ); } - const lines = []; - if (lineLength === 0) { - lines.push(stringValues.join("\t")); - } else { - for (let i = 0; i < stringValues.length; i += lineLength) { - lines.push(stringValues.slice(i, i+lineLength).join("\t")); - } - } - - const paddingBytes = []; - for (let i = 0; i < stringValues.length % instructionLength; i++) { - paddingBytes.push('U8 0'); - } - const paddingString = paddingBytes.length === 0 ? '' : `\n${paddingBytes.join('\t')} ; padding to preserve ${8 * instructionLength} bits alignment` - - return `${label}: -${lines.join("\n")} -${label}_end:${paddingString}` + return lines.join("\n"); } /** * Return the image pixels data (excluding all other fields) from a given PPM file - * + * * The implementation differ from the format definition (https://netpbm.sourceforge.net/doc/ppm.html) but match the file created by GIMP. */ async function getDataFromPPM(path: string): Promise { @@ -189,7 +265,7 @@ async function getDataFromPPM(path: string): Promise { let offset = 0; // magic number - while(!whitespaces.includes(content[offset])) { + while (!whitespaces.includes(content[offset])) { offset++; } @@ -197,7 +273,7 @@ async function getDataFromPPM(path: string): Promise { // potential comment if (content[offset] === hashtag) { - while(!endOfComment.includes(content[offset])) { + while (!endOfComment.includes(content[offset])) { offset++; } @@ -205,25 +281,73 @@ async function getDataFromPPM(path: string): Promise { } // image width - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // image height - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace // maximum color value - while(numbers.includes(content[offset])) { + while (numbers.includes(content[offset])) { offset++; } offset++; // whitespace return content.slice(offset, content.length); +} + +function printUsage(into: (message: string) => void) { + into(`Usage: +\tbuild.ts [--minify] +\t\tMerge the hardcoded files and output the result to stdout. The "--minify" option can be used to strip the result of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation. +\tbuild.ts --help +\t\tPrint this message`); +} + +/** + * SCRIPT + */ + +if (Deno.args.length > 1) { + printUsage(console.error); + Deno.exit(1); +} + +let minify = false; +if (Deno.args.length === 1) { + switch (Deno.args[0]) { + case "--help": + printUsage(console.log); + Deno.exit(0); + break; + case "--minify": + minify = true; + break; + default: + printUsage(console.error); + Deno.exit(1); + break; + } +} + +const encoder = new TextEncoder(); +const stdoutWritable = new WritableStream({ + async write(chunk) { + await Deno.stdout.write(encoder.encode(chunk)); + }, +}); + +if (minify) { + const minifier = new Minifier(stdoutWritable); + await build(minifier.stream.writable.getWriter()); +} else { + await build(stdoutWritable.getWriter()); } \ No newline at end of file diff --git a/readme.md b/readme.md index 402841e..d65be4f 100755 --- a/readme.md +++ b/readme.md @@ -31,10 +31,7 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat Usage example: ```sh -./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm - -# script output can easily be piped to strip it off all comments, leading whitespace, double spaces and empty lines: -./build.ts | sed -r 's/\s*;.*//' | sed -r 's/^\s+//' | sed -r 's/ +/ /' | sed -r '/^\s*$/d' > result.asm +./build.ts --minify > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm ``` The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way.