#!/usr/bin/env -S deno run --allow-read --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 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", "src/lib/sprite_rotation.asm", ]; const reservedSpacePath = "src/reserved_space.asm"; 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(output: WritableStreamDefaultWriter) { for (const path of asmFilePaths) { const file = await Deno.open(path); const readable = file.readable.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); await output.write(` ;/******************************* ;* ${path} ;*******************************/ `); for await (const line of readable) { if (!line.startsWith("include ")) { await output.write(`${line}\n`); } } } const asmRawRepresentations = await Promise.all([ ppmToAsm("map", "map", mapPath), ...Object.entries(spritesPath).map(([label, path]) => ppmToAsm("sprite", label, path)), ]); await output.write(asmRawRepresentations.join("\n\n")); await output.write(` ; ; RESERVED RAM SPACE ; `); await output.write(await Deno.readTextFile(reservedSpacePath)); await output.close(); } 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({ 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()); }