#!/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"; 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 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 spritesMatchingStrings = { 0x00_00_00: '0b000_000_00', 0x00_00_FF: '0b000_000_11', 0x00_FF_00: '0b000_111_00', 0x77_77_77: '0b010_011_10', 0xFF_00_00: '0b111_000_00', 0xFF_00_FF: '0b111_000_11', 0xFF_80_00: '0b111_100_00', 0xFF_FF_00: '0b111_110_00', 0xFF_FF_FF: '0b111_111_11', }; 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', }; /** * START OF BUILD */ for (const path of asmFilePaths) { const file = await Deno.open(path); const readable = file.readable.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextLineStream()); console.log(` ;/******************************* ;* ${path} ;*******************************/ `); for await (const line of readable) { if (!line.startsWith("include ")) { console.log(line); } } } const asmRawRepresentations = []; 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, spritesMatchingStrings, spriteWidth)); } console.log(asmRawRepresentations.join("\n\n")); console.log(` ; ; RESERVED RAM SPACE ; `); console.log(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 * * @returns The ASM representation of that raw data, pre- and post-fixed by the given label */ function bytesToAsmConstU8(label: string, data: Uint8Array, matchingStrings: Record, lineLength = 0): string { lineLength ??= Infinity; 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)`); Deno.exit(1); } stringValues.push(`U8 ${matchingStrings[pixelValue]}`); } 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 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 { const endOfComment = [ 0x0a, 0x0d, ]; const whitespaces = [ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20, ]; const numbers = [ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, ]; const hashtag = 0x23; 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++; } offset++; // end of comment } // 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); }