227 lines
5.6 KiB
TypeScript
Executable File
227 lines
5.6 KiB
TypeScript
Executable File
#!/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: '0b011_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<number, string>, 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<Uint8Array> {
|
|
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);
|
|
} |