316
build.ts
316
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<string> = [];
|
||||
stream;
|
||||
|
||||
console.log(`
|
||||
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) {
|
||||
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<number, string>, lineLength = 0): string {
|
||||
lineLength ??= Infinity;
|
||||
function bytesToAsmConstU8(
|
||||
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)",
|
||||
);
|
||||
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<Uint8Array> {
|
||||
@@ -189,7 +265,7 @@ async function getDataFromPPM(path: string): Promise<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
|
||||
// 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<Uint8Array> {
|
||||
}
|
||||
|
||||
// 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<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());
|
||||
}
|
||||
Reference in New Issue
Block a user