263 lines
6.9 KiB
TypeScript
Executable File
263 lines
6.9 KiB
TypeScript
Executable File
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run
|
|
|
|
import { TextLineStream } from "jsr:@std/streams@1.1.0";
|
|
|
|
/**
|
|
* CONFIG
|
|
*/
|
|
|
|
const instructionLength = 4; // in bytes
|
|
const rawDataInstructionLengthPrefix = "U32";
|
|
|
|
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_coin": "assets/sprites/coin.ppm",
|
|
};
|
|
|
|
/**
|
|
* END OF CONFIG
|
|
*/
|
|
|
|
class Minifier {
|
|
private buffer = "";
|
|
private lines: Array<string> = [];
|
|
private insideComment = false;
|
|
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());
|
|
controller.enqueue(this.lines.join("\n") + "\n");
|
|
this.lines = [];
|
|
},
|
|
});
|
|
|
|
this.stream.readable.pipeTo(writableStream);
|
|
}
|
|
|
|
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
|
|
|
|
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 ppmToAsm(
|
|
mode: "map" | "sprite",
|
|
label: string,
|
|
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);
|
|
}
|
|
|
|
return decoder.decode(stdout);
|
|
}
|
|
|
|
async function build(
|
|
pwd: string,
|
|
path: string,
|
|
output: WritableStreamDefaultWriter<string>,
|
|
) {
|
|
const includeExpressionRegex = /^\s*include\s+([^\s;]+)/;
|
|
|
|
const file = await Deno.open(`${pwd}/${path}`);
|
|
const readable = file.readable.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(new TextLineStream());
|
|
|
|
for await (const line of readable) {
|
|
const match = line.match(includeExpressionRegex);
|
|
if (match === null) {
|
|
await output.write(`${line}\n`);
|
|
continue;
|
|
}
|
|
|
|
await build(pwd, `${match[1]}.asm`, output);
|
|
}
|
|
}
|
|
|
|
function printUsage(into: (message: string) => void) {
|
|
into(`Usage:
|
|
\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
|
|
|
|
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) {
|
|
printUsage(console.error);
|
|
Deno.exit(1);
|
|
}
|
|
|
|
let minify = false;
|
|
let convertPPM = false;
|
|
let path: string | undefined = undefined;
|
|
|
|
Deno.args.forEach((arg, index) => {
|
|
switch (arg) {
|
|
case "--help":
|
|
printUsage(console.log);
|
|
Deno.exit(0);
|
|
break;
|
|
case "--minify":
|
|
minify = true;
|
|
break;
|
|
case "--ppm":
|
|
convertPPM = true;
|
|
break;
|
|
|
|
default:
|
|
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();
|
|
const stdoutWritable = new WritableStream<string>({
|
|
async write(chunk) {
|
|
await Deno.stdout.write(encoder.encode(chunk));
|
|
},
|
|
});
|
|
|
|
const writer = minify
|
|
? (new Minifier(stdoutWritable)).stream.writable.getWriter()
|
|
: stdoutWritable.getWriter();
|
|
|
|
await build(`${Deno.cwd()}/src`, path, writer);
|
|
await writer.close();
|