Merge branch 'main' into res-0-prerendered

This commit is contained in:
2026-05-01 21:48:24 +02:00
30 changed files with 938 additions and 693 deletions

346
build.ts
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-read
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run
import { TextLineStream } from "jsr:@std/streams@1.1.0";
@@ -9,54 +9,22 @@ import { TextLineStream } from "jsr:@std/streams@1.1.0";
const instructionLength = 4; // in bytes
const rawDataInstructionLengthPrefix = "U32";
const asmFilePaths = [
"src/consts/arch.asm",
"src/consts/game.asm",
"src/consts/keyboard.asm",
"src/consts/map.asm",
"src/consts/scene.asm",
"src/consts/screen.asm",
"src/consts/sprites.asm",
"src/init.asm",
"src/main.asm",
"src/lib/game.asm",
"src/lib/drawing.asm",
];
const initialScreenPath = "assets/screen.ppm";
const reservedSpacePath = "src/reserved_space.asm";
const mapMatchingValues = {
0x00_00_00: 0, // Empty
0xFF_00_00: 1, // Wall
0xFF_FF_00: 2, // Coin
};
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 ppmToAsmScriptPath = "./ppmToAsm.ts";
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_robot_up": "assets/sprites/robot_up.ppm",
"sprite_robot_right": "assets/sprites/robot_right.ppm",
"sprite_robot_down": "assets/sprites/robot_down.ppm",
"sprite_robot_left": "assets/sprites/robot_left.ppm",
"sprite_ghost_up": "assets/sprites/ghost_up.ppm",
"sprite_ghost_right": "assets/sprites/ghost_right.ppm",
"sprite_ghost_down": "assets/sprites/ghost_down.ppm",
"sprite_ghost_left": "assets/sprites/ghost_left.ppm",
"sprite_coin": "assets/sprites/coin.ppm",
"screen": "assets/screen.ppm",
};
/**
@@ -66,6 +34,7 @@ const spritesPath = {
class Minifier {
private buffer = "";
private lines: Array<string> = [];
private insideComment = false;
stream;
constructor(writableStream: WritableStream) {
@@ -78,6 +47,8 @@ class Minifier {
flush: (controller) => {
this.buffer += "\n";
controller.enqueue(this.processChunk());
controller.enqueue(this.lines.join("\n") + "\n");
this.lines = [];
},
});
@@ -85,6 +56,32 @@ class Minifier {
}
private processChunk(): string {
const commentFullRegex = /\/\*.*\*\//;
const commentStartRegex = /(.*)\/\*(.*)/;
const commentEndRegex = /(.*)\*\/(.*)/;
if (this.insideComment) {
const matches = this.buffer.match(commentEndRegex);
if (matches === null) {
this.buffer = '';
return '';
}
this.buffer = matches[2];
this.insideComment = false;
}
if (!this.insideComment) {
this.buffer = this.buffer.replace(commentFullRegex, '');
const matches = this.buffer.match(commentStartRegex);
if (matches !== null) {
this.buffer = matches[1];
this.insideComment = true;
return '';
}
}
const newLines = this.buffer.split("\n");
this.buffer = newLines.pop() as string; // keep the last (potentially incomplete) line
@@ -133,200 +130,82 @@ class Minifier {
}
}
async function build(output: WritableStreamDefaultWriter<string>) {
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 ")) {
output.write(`${line}\n`);
}
}
}
const asmRawRepresentations = [];
const mapBytes = await getDataFromPPM(mapPath);
asmRawRepresentations.push(
bytesToAsmConstU8("map", mapBytes, mapMatchingValues),
);
for (const [label, path] of Object.entries(spritesPath)) {
const bytes = await getDataFromPPM(path);
asmRawRepresentations.push(
bytesToAsmConstU8(label, bytes, spritesMatchingValues),
);
}
output.write(asmRawRepresentations.join("\n\n"));
output.write(`
;
; RESERVED RAM SPACE
;
`);
const screenBytes = await getDataFromPPM(initialScreenPath);
output.write(bytesToAsmConstU8("screen", screenBytes, spritesMatchingValues));
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 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(
async function ppmToAsm(
mode: "map" | "sprite",
label: string,
data: Uint8Array,
matchingValues: Record<number, number>,
): 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)",
);
path: string,
): Promise<string> {
const command = new Deno.Command(ppmToAsmScriptPath, {
args: [
instructionLength.toString(),
mode,
label,
path,
],
});
const { code, stdout, stderr } = await command.output();
const decoder = new TextDecoder();
if (code !== 0) {
console.error(`ppmToAsm script returned non-zero value: ${code}`);
console.error(`STDERR: \n${decoder.decode(stderr)}`);
Deno.exit(1);
}
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);
}
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`,
);
}
return lines.join("\n");
return decoder.decode(stdout);
}
/**
* 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<Uint8Array> {
const endOfComment = [
0x0a,
0x0d,
];
const whitespaces = [
0x09,
0x0a,
0x0b,
0x0c,
0x0d,
0x20,
];
const numbers = [
0x30,
0x31,
0x32,
0x33,
0x34,
0x35,
0x36,
0x37,
0x38,
0x39,
];
async function build(
pwd: string,
path: string,
output: WritableStreamDefaultWriter<string>,
) {
const includeExpressionRegex = /^\s*include\s+([^\s;]+)/;
const hashtag = 0x23;
const file = await Deno.open(`${pwd}/${path}`);
const readable = file.readable.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream());
const content = await Deno.readFile(path);
let offset = 0;
// magic number
while (!whitespaces.includes(content[offset])) {
offset++;
}
offset++; // whitespace
// potential comment
if (content[offset] === hashtag) {
while (!endOfComment.includes(content[offset])) {
offset++;
for await (const line of readable) {
const match = line.match(includeExpressionRegex);
if (match === null) {
await output.write(`${line}\n`);
continue;
}
offset++; // end of comment
await build(pwd, `${match[1]}.asm`, output);
}
// image width
while (numbers.includes(content[offset])) {
offset++;
}
offset++; // whitespace
// image height
while (numbers.includes(content[offset])) {
offset++;
}
offset++; // whitespace
// maximum color value
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 [options...] {file}
\t\tRead the given file and recursively include other '.asm' files to produce a single output.
\tbuild.ts --help
\t\tPrint this message`);
\t\tPrint this message
Options:
\t--minify\tWhen used, the result will be stripped of all comments, leading/trailing double whitespaces, empty lines and use the more compact raw data representation.
\t--ppm \tWhen used, the hardcoded ppm files will be translated to their matching Symfony assembly reprensentation into src/dist.`);
}
/**
* SCRIPT
*/
if (Deno.args.length > 1) {
if (Deno.args.length < 1) {
printUsage(console.error);
Deno.exit(1);
}
let minify = false;
if (Deno.args.length === 1) {
switch (Deno.args[0]) {
let convertPPM = false;
let path: string | undefined = undefined;
Deno.args.forEach((arg, index) => {
switch (arg) {
case "--help":
printUsage(console.log);
Deno.exit(0);
@@ -334,11 +213,42 @@ if (Deno.args.length === 1) {
case "--minify":
minify = true;
break;
case "--ppm":
convertPPM = true;
break;
default:
printUsage(console.error);
Deno.exit(1);
if (index !== Deno.args.length - 1) {
printUsage(console.error);
Deno.exit(1);
}
path = arg;
break;
}
});
if (path === undefined) {
printUsage(console.error);
Deno.exit(1);
}
if (convertPPM) {
await Deno.mkdir(`${Deno.cwd()}/src/dist`, { recursive: true });
const mapPromise = async () => {
const assembly = await ppmToAsm("map", "map", mapPath);
await Deno.writeTextFile(`${Deno.cwd()}/src/dist/map.asm`, assembly);
};
const spritePromises = Object.entries(spritesPath).map(async ([label, path]) => {
const assembly = await ppmToAsm("sprite", label, path);
const filename = path.split("/").pop()?.split('.')[0];
await Deno.writeTextFile(`${Deno.cwd()}/src/dist/${filename}.asm`, assembly);
});
await Promise.all([
mapPromise(),
...spritePromises,
]);
}
const encoder = new TextEncoder();
@@ -348,9 +258,9 @@ const stdoutWritable = new WritableStream<string>({
},
});
if (minify) {
const minifier = new Minifier(stdoutWritable);
await build(minifier.stream.writable.getWriter());
} else {
await build(stdoutWritable.getWriter());
}
const writer = minify
? (new Minifier(stdoutWritable)).stream.writable.getWriter()
: stdoutWritable.getWriter();
await build(`${Deno.cwd()}/src`, path, writer);
await writer.close();