add minify flag to build script

This commit is contained in:
2026-04-27 12:36:00 +02:00
parent cdc3a8e3a1
commit f0b43f7b7c
2 changed files with 222 additions and 101 deletions

268
build.ts
View File

@@ -1,17 +1,13 @@
#!/usr/bin/env -S deno run --allow-read #!/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"; import { TextLineStream } from "jsr:@std/streams@1.1.0";
/**
* CONFIG
*/
const instructionLength = 4; // in bytes const instructionLength = 4; // in bytes
const mapWidth = 15; const rawDataInstructionLengthPrefix = "U32";
const spriteWidth = 17;
const asmFilePaths = [ const asmFilePaths = [
"src/consts/arch.asm", "src/consts/arch.asm",
@@ -28,51 +24,122 @@ const asmFilePaths = [
"src/lib/game.asm", "src/lib/game.asm",
"src/lib/drawing.asm", "src/lib/drawing.asm",
]; ];
const initialScreenPath = 'assets/screen.ppm'; const initialScreenPath = "assets/screen.ppm";
const reservedSpacePath = 'src/reserved_space.asm'; const reservedSpacePath = "src/reserved_space.asm";
const mapMatchingStrings = { const mapMatchingValues = {
0x00_00_00: '0', // Empty 0x00_00_00: 0, // Empty
0xFF_00_00: '1', // Wall 0xFF_00_00: 1, // Wall
0xFF_FF_00: '2', // Coin 0xFF_FF_00: 2, // Coin
}; };
const spritesMatchingStrings = { const spritesMatchingValues = {
0x00_00_00: ' 0', // 0b000_000_00 0x00_00_00: 0, // 0b000_000_00
0x00_00_FF: ' 3', // 0b000_000_11 0x00_00_FF: 3, // 0b000_000_11
0x00_FF_00: ' 28', // 0b000_111_00 0x00_FF_00: 28, // 0b000_111_00
0x99_50_00: ' 68', // 0b010_001_00 0x99_50_00: 68, // 0b010_001_00
0x77_77_77: '110', // 0b011_011_10 0x77_77_77: 110, // 0b011_011_10
0xB3_B3_B3: '146', // 0b100_100_10 0xB3_B3_B3: 146, // 0b100_100_10
0xFF_00_00: '224', // 0b111_000_00 0xFF_00_00: 224, // 0b111_000_00
0xFF_00_FF: '227', // 0b111_000_11 0xFF_00_FF: 227, // 0b111_000_11
0xFF_80_00: '240', // 0b111_100_00 0xFF_80_00: 240, // 0b111_100_00
0xFF_FF_00: '248', // 0b111_110_00 0xFF_FF_00: 248, // 0b111_110_00
0xFF_FF_FF: '255', // 0b111_111_11 0xFF_FF_FF: 255, // 0b111_111_11
}; };
const mapPath = 'assets/map.ppm'; const mapPath = "assets/map.ppm";
const spritesPath = { const spritesPath = {
'sprite_empty': 'assets/sprites/empty.ppm', "sprite_empty": "assets/sprites/empty.ppm",
'sprite_unimplemented': 'assets/sprites/placeholder.ppm', "sprite_unimplemented": "assets/sprites/placeholder.ppm",
'sprite_wall_end': 'assets/sprites/wall_end.ppm', "sprite_wall_end": "assets/sprites/wall_end.ppm",
'sprite_wall_straight': 'assets/sprites/wall_straight.ppm', "sprite_wall_straight": "assets/sprites/wall_straight.ppm",
'sprite_wall_corner': 'assets/sprites/wall_corner.ppm', "sprite_wall_corner": "assets/sprites/wall_corner.ppm",
'sprite_robot': 'assets/sprites/robot.ppm', "sprite_robot": "assets/sprites/robot.ppm",
'sprite_ghost': 'assets/sprites/ghost.ppm', "sprite_ghost": "assets/sprites/ghost.ppm",
'sprite_coin': 'assets/sprites/coin.ppm', "sprite_coin": "assets/sprites/coin.ppm",
}; };
/** /**
* START OF BUILD * END OF CONFIG
*/ */
class Minifier {
private buffer = "";
private lines: Array<string> = [];
stream;
constructor(writableStream: WritableStream) {
this.stream = new TransformStream<string>({
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<string> = [];
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<string>) {
for (const path of asmFilePaths) { for (const path of asmFilePaths) {
const file = await Deno.open(path); const file = await Deno.open(path);
const readable = file.readable.pipeThrough(new TextDecoderStream()) const readable = file.readable.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream()); .pipeThrough(new TextLineStream());
console.log(` output.write(`
;/******************************* ;/*******************************
;* ${path} ;* ${path}
;*******************************/ ;*******************************/
@@ -80,34 +147,38 @@ for (const path of asmFilePaths) {
for await (const line of readable) { for await (const line of readable) {
if (!line.startsWith("include ")) { if (!line.startsWith("include ")) {
console.log(line); output.write(`${line}\n`);
} }
} }
} }
const asmRawRepresentations = []; const asmRawRepresentations = [];
const mapBytes = await getDataFromPPM(mapPath); const mapBytes = await getDataFromPPM(mapPath);
asmRawRepresentations.push(bytesToAsmConstU8('map', mapBytes, mapMatchingStrings, mapWidth)); asmRawRepresentations.push(
bytesToAsmConstU8("map", mapBytes, mapMatchingValues),
);
for (const [label, path] of Object.entries(spritesPath)) { for (const [label, path] of Object.entries(spritesPath)) {
const bytes = await getDataFromPPM(path); const bytes = await getDataFromPPM(path);
asmRawRepresentations.push(bytesToAsmConstU8(label, bytes, spritesMatchingStrings, spriteWidth)); asmRawRepresentations.push(
bytesToAsmConstU8(label, bytes, spritesMatchingValues),
);
} }
console.log(asmRawRepresentations.join("\n\n")); output.write(asmRawRepresentations.join("\n\n"));
console.log(` output.write(`
; ;
; RESERVED RAM SPACE ; RESERVED RAM SPACE
; ;
`); `);
const screenBytes = await getDataFromPPM(initialScreenPath); const screenBytes = await getDataFromPPM(initialScreenPath);
console.log(bytesToAsmConstU8('screen', screenBytes, spritesMatchingStrings, 1)); output.write(bytesToAsmConstU8("screen", screenBytes, spritesMatchingValues));
console.log(await Deno.readTextFile(reservedSpacePath)) output.write(await Deno.readTextFile(reservedSpacePath));
}
/** /**
* Return an ASM constant (U8) representation of the given data * Return an ASM constant (U8) representation of the given data
@@ -116,43 +187,48 @@ console.log(await Deno.readTextFile(reservedSpacePath))
* *
* @param label The label to write before the first raw data * @param label The label to write before the first raw data
* @param data An array of bytes to interprete as 24 bits values * @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 matchingValues 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
* *
* @returns The ASM representation of that raw data, pre- and post-fixed by the given label * @returns The ASM representation of that raw data, pre- and post-fixed by the given label
*/ */
function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record<number, string>, lineLength = 0): string { function bytesToAsmConstU8(
lineLength ??= Infinity; label: string,
data: Uint8Array,
const stringValues = []; matchingValues: Record<number, number>,
for (let i = 0; i < data.length - 2; i += 3) { ): string {
const pixelValue = (data[i] << 16) + (data[i+1] << 8) + (data[i+2]) if (data.length % 3 !== 0) {
if (!Object.hasOwn(matchingStrings, pixelValue)) { console.error(
console.error(`No matching value defined for "${pixelValue.toString(2)}" (which is ${label}'s ${i} pixel)`); "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); Deno.exit(1);
} }
stringValues.push(`U8 ${matchingStrings[pixelValue]}`); 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);
} }
const lines = []; lines.push(`U8 ${matchingValues[pixelValue]}`);
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"));
} }
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 paddingBytes = []; return lines.join("\n");
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}`
} }
/** /**
@@ -230,3 +306,51 @@ async function getDataFromPPM(path: string): Promise<Uint8Array> {
return content.slice(offset, content.length); 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<string>({
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());
}

View File

@@ -26,10 +26,7 @@ It also convert the PPM assets (map and sprites) into ASM raw values representat
Usage example: Usage example:
```sh ```sh
./build.ts > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm ./build.ts --minify > /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
``` ```
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. 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.