#!/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 = []; stream; 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()); controller.enqueue(this.lines.join("\n") + "\n"); this.lines = []; }, }); 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 ppmToAsm( mode: "map" | "sprite", label: string, path: string, ): Promise { 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, ) { 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({ 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();