move ppm to asm representation to its own script
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
191
build.ts
191
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";
|
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 instructionLength = 4; // in bytes
|
||||||
const rawDataInstructionLengthPrefix = "U32";
|
const rawDataInstructionLengthPrefix = "U32";
|
||||||
|
|
||||||
|
const ppmToAsmScriptPath = './ppmToAsm.ts';
|
||||||
|
|
||||||
const asmFilePaths = [
|
const asmFilePaths = [
|
||||||
"src/consts/arch.asm",
|
"src/consts/arch.asm",
|
||||||
"src/consts/game.asm",
|
"src/consts/game.asm",
|
||||||
@@ -27,26 +29,6 @@ const asmFilePaths = [
|
|||||||
];
|
];
|
||||||
const reservedSpacePath = "src/reserved_space.asm";
|
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 mapPath = "assets/map.ppm";
|
||||||
const spritesPath = {
|
const spritesPath = {
|
||||||
"sprite_empty": "assets/sprites/empty.ppm",
|
"sprite_empty": "assets/sprites/empty.ppm",
|
||||||
@@ -135,6 +117,29 @@ class Minifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ppmToAsm(mode: "map" | "sprite", label: string, path: string): Promise<string> {
|
||||||
|
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<string>) {
|
async function build(output: WritableStreamDefaultWriter<string>) {
|
||||||
for (const path of asmFilePaths) {
|
for (const path of asmFilePaths) {
|
||||||
const file = await Deno.open(path);
|
const file = await Deno.open(path);
|
||||||
@@ -154,19 +159,10 @@ async function build(output: WritableStreamDefaultWriter<string>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const asmRawRepresentations = [];
|
const asmRawRepresentations = await Promise.all([
|
||||||
|
ppmToAsm("map", "map", mapPath),
|
||||||
const mapBytes = await getDataFromPPM(mapPath);
|
...Object.entries(spritesPath).map(([label, path]) => ppmToAsm("sprite", label, path)),
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await output.write(asmRawRepresentations.join("\n\n"));
|
await output.write(asmRawRepresentations.join("\n\n"));
|
||||||
|
|
||||||
@@ -180,133 +176,6 @@ async function build(output: WritableStreamDefaultWriter<string>) {
|
|||||||
await output.close();
|
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<number, 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<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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printUsage(into: (message: string) => void) {
|
function printUsage(into: (message: string) => void) {
|
||||||
into(`Usage:
|
into(`Usage:
|
||||||
\tbuild.ts [--minify]
|
\tbuild.ts [--minify]
|
||||||
|
|||||||
184
ppmToAsm.ts
Executable file
184
ppmToAsm.ts
Executable file
@@ -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<number, number>,
|
||||||
|
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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
18
readme.md
18
readme.md
@@ -25,16 +25,26 @@ The following branches contains variations, allowing comparisons of performances
|
|||||||
|
|
||||||
## Build script
|
## 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:
|
Usage example:
|
||||||
```sh
|
```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
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user