diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/build.ts b/build.ts index d5a7dff..dc881db 100755 --- a/build.ts +++ b/build.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S deno run --allow-read +#!/usr/bin/env -S deno run --allow-read --allow-run import { TextLineStream } from "jsr:@std/streams@1.1.0"; @@ -9,6 +9,8 @@ import { TextLineStream } from "jsr:@std/streams@1.1.0"; const instructionLength = 4; // in bytes const rawDataInstructionLengthPrefix = "U32"; +const ppmToAsmScriptPath = './ppmToAsm.ts'; + const asmFilePaths = [ "src/consts/arch.asm", "src/consts/game.asm", @@ -27,26 +29,6 @@ const asmFilePaths = [ ]; const reservedSpacePath = "src/reserved_space.asm"; -const mapMatchingValues = { - 0x00_00_00: 0, // Empty - 0xFF_00_00: 1, // Wall - 0xFF_FF_00: 2, // Coin -}; - -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 spritesPath = { "sprite_empty": "assets/sprites/empty.ppm", @@ -135,6 +117,29 @@ class Minifier { } } +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); @@ -154,19 +159,10 @@ async function build(output: WritableStreamDefaultWriter) { } } - const asmRawRepresentations = []; - - const mapBytes = await getDataFromPPM(mapPath); - asmRawRepresentations.push( - bytesToAsmConstU8("map", mapBytes, mapMatchingValues), - ); - - for (const [label, path] of Object.entries(spritesPath)) { - const bytes = await getDataFromPPM(path); - asmRawRepresentations.push( - bytesToAsmConstU8(label, bytes, spritesMatchingValues), - ); - } + 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")); @@ -180,133 +176,6 @@ async function build(output: WritableStreamDefaultWriter) { await output.close(); } -/** - * 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 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, - matchingValues: Record, -): 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 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); - } - - 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`, - ); - } - - 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 { - 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); -} - function printUsage(into: (message: string) => void) { into(`Usage: \tbuild.ts [--minify] diff --git a/ppmToAsm.ts b/ppmToAsm.ts new file mode 100755 index 0000000..376a514 --- /dev/null +++ b/ppmToAsm.ts @@ -0,0 +1,184 @@ +#!/usr/bin/env -S deno run --allow-read + +const ppmCorrespondanceTables = { + map: { + 0x00_00_00: 0, // Empty + 0xFF_00_00: 1, // Wall + 0xFF_FF_00: 2, // Coin + }, + sprite: { + 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 + }, +}; + +/** + * Return an ASM constants (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 matchingValues An associative array with a string representation for each possible 24 bit data value + * @param instructionLength The size / length in byte of an instruction. Used to pad result to ensure data stays aligned + * + * @returns The ASM representation of that raw data, pre- and post-fixed by the given label + */ +function ppmBytesToCorrespondingAsmConstU8( + label: string, + data: Uint8Array, + matchingValues: Record, + instructionLength: 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 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); + } + + 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`, + ); + } + + 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 { + 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); +} + +function printUsage(into: (message: string) => void) { + into(`Usage: +\tppmToAsm.ts {instruction length} {map|sprite} {label} {file} +\t\tConvert the given PPM file into a Symfony assembly representation using the given correspondance table. Result will be padded with "U8 0"s to ensure alignment to given instruction length. +\tppmToAsm.ts --help +\t\tPrint this message`); +} + +/** + * SCRIPT + */ + +if (Deno.args.length !== 4) { + printUsage(console.error); + Deno.exit(1); +} + +if (!(Deno.args[1] in ppmCorrespondanceTables)) { + console.error(`"${Deno.args[1]}" is not a valid correspondance table.\n`); + printUsage(console.error); + Deno.exit(1); +} + +const mode = Deno.args[1] as keyof typeof ppmCorrespondanceTables; +const correspondanceTable = ppmCorrespondanceTables[mode]; +const label = Deno.args[2]; +const path = Deno.args[3]; +const instructionLength = parseInt(Deno.args[0]); + +const ppmData = await getDataFromPPM(path); +const asm = ppmBytesToCorrespondingAsmConstU8(label, ppmData, correspondanceTable, instructionLength); +console.log(asm); diff --git a/readme.md b/readme.md index d65be4f..d5a85d8 100755 --- a/readme.md +++ b/readme.md @@ -25,16 +25,26 @@ The following branches contains variations, allowing comparisons of performances ## Build script -File inclusion is currently not supported by the game, so a `build.ts` script it provided that will concatenate the content of the hard-coded `.asm` files and output the result on stdout. +Both scripts uses Deno's API, so you'll need to have a `deno` binary in your PATH. -It also convert the PPM assets (map and sprites) into ASM raw values representation and append it after all the code. +### PPM to Symfony assembly + +Output the Symfony assembly representation of the given PPM file. Usage example: ```sh -./build.ts --minify > /path/to/the/game/schematics/architecture/Symfony/sandbox/main.asm +./ppmToAsm.ts map map ./assets/map.ppm > ./dist/map.asm +./ppmToAsm.ts sprite robot ./assets/sprites/robot.ppm > ./dist/robot.asm ``` -The script is build over Deno's API, so you'll need to have a `deno` binary in your PATH to use it that way. +### Bundle and minify + +Bundle all `.asm` and `.ppm` files and output the (optionaly minified) result. + +Usage example: +```sh +./build.ts --minify > pacman.asm +``` ## Documentation