Skip to content

Commit

Permalink
Add unit tests for room gfx nametable and a serialiser 📡.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarty committed May 2, 2024
1 parent 4a3db62 commit 6d77cce
Show file tree
Hide file tree
Showing 5 changed files with 409 additions and 42 deletions.
52 changes: 10 additions & 42 deletions src/lib/parser/parseRooms.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Parser from './parser.js';
import parseRoomHeader from './room/parseRoomHeader.js';
import parseRoomNametable from './room/parseRoomNametable.js';
import parseRoomBoxes from './room/parseRoomBoxes.js';
import parseRoomMatrix from './room/parseRoomMatrix.js';

Expand Down Expand Up @@ -63,50 +64,17 @@ const parseRooms = (arrayBuffer, i = 0, offset = 0, characters = {}) => {
}

// Parse gfx nametable.
const nametableParser = new Parser(arrayBuffer.slice(nametableOffs));

const tileset = nametableParser.getUint8();

const palette = [];
const nametableObj = Array(16);
for (let i = 0; i < 16; i++) {
palette[i] = nametableParser.getUint8();
}
for (let i = 0; i < 16; i++) {
nametableObj[i] = Array(64).fill(0);
nametableObj[i][0] = 0;
nametableObj[i][1] = 0;
let n = 0;
while (n < width) {
const loop = nametableParser.getUint8();
if (loop & 0x80) {
for (let j = 0; j < (loop & 0x7f); j++) {
nametableObj[i][2 + n++] = nametableParser.getUint8();
}
} else {
const data = nametableParser.getUint8();
for (let j = 0; j < (loop & 0x7f); j++) {
nametableObj[i][2 + n++] = data;
}
}
}
}

const nametable = {
tileset,
palette,
nametableObj,
};
const { nametable, nametableMap } = parseRoomNametable(
arrayBuffer.slice(nametableOffs, attrOffs),
nametableOffs,
width,
);

map.push({
type: 'nametable',
from: nametableOffs,
to: nametableOffs + nametableParser.pointer - 1,
});
map.push(nametableMap);

assert(
nametableOffs + nametableParser.pointer === attrOffs,
'name table overlaps on attributes table.',
nametableMap.to + 1 === attrOffs,
'Gfx nametable overlaps on attributes table.',
);

// Parse gfx attrtable.
Expand Down Expand Up @@ -222,7 +190,7 @@ const parseRooms = (arrayBuffer, i = 0, offset = 0, characters = {}) => {
const objectImagesStart = Math.min(...objectImagesOffs);
const objectsStart = Math.min(...objectsOffs);

// End rang values will be set later.
// End range values will be set later.
const objectImageMap = {
type: 'objectImages',
from: objectImagesStart,
Expand Down
70 changes: 70 additions & 0 deletions src/lib/parser/room/parseRoomNametable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Parser from '../parser.js';

const assert = console.assert;

const parseRoomNametable = (arrayBuffer, offset = 0, width = 0) => {
const parser = new Parser(arrayBuffer);
const tileset = parser.getUint8();

const palette = [];
for (let i = 0; i < 16; i++) {
palette[i] = parser.getUint8();
}

const nametableObj = Array(16);
for (let i = 0; i < 16; i++) {
nametableObj[i] = Array(64).fill(0);
nametableObj[i][0] = 0;
nametableObj[i][1] = 0;

assert(
nametableObj[i][0] === 0,
'Gfx nametable strip does not start with 0x00 0x00.',
);
assert(
nametableObj[i][1] === 0,
'Gfx nametable strip does not start with 0x00 0x00.',
);

let n = 0;
while (n < width) {
const loop = parser.getUint8();
if (loop & 0x80) {
for (let j = 0; j < (loop & 0x7f); j++) {
nametableObj[i][2 + n++] = parser.getUint8();
}
} else {
const data = parser.getUint8();
for (let j = 0; j < (loop & 0x7f); j++) {
nametableObj[i][2 + n++] = data;
}
}
}

assert(
nametableObj[i][62] === 0,
'Gfx nametable strip does not end with 0x00 0x00.',
);
assert(
nametableObj[i][63] === 0,
'Gfx nametable strip does not end with 0x00 0x00.',
);
}

const nametableMap = {
type: 'nametable',
from: offset,
to: offset + parser.pointer - 1,
};

return {
nametable: {
tileset,
palette,
nametableObj,
},
nametableMap,
};
};

export default parseRoomNametable;
61 changes: 61 additions & 0 deletions src/lib/serialiser/room/serialiseRoomNametable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Serialiser from '../serialiser.js';

const serialiseRoomNametable = (nametable = {}) => {
const serialiser = new Serialiser();
const { tileset, palette, nametableObj } = nametable;

serialiser.setUint8(tileset);

for (let i = 0; i < 16; i++) {
serialiser.setUint8(palette[i]);
}

for (let i = 0; i < 16; i++) {
serialiseOneStrip(nametableObj[i].slice(2, 62), serialiser);
}

return serialiser.buffer;
};

// Serialise one strip using the compression algorithm.
const serialiseOneStrip = (nametableObj, serialiser) => {
let n = 0;
while (n < 60) {
let initialTile = nametableObj[n];
let loop = 0;

// Look 2 tiles ahead.
if (
nametableObj[n] !== nametableObj[n + 1] ||
nametableObj[n] !== nametableObj[n + 2]
) {
// The next 3 tiles are different. Count how many unique tiles there are.
// It stops once it encounters 3 identical tiles in a row.
const initialN = n;

do {
loop++;
initialTile = nametableObj[++n];
} while (
initialTile !== nametableObj[n + 1] ||
initialTile !== nametableObj[n + 2]
);

serialiser.setUint8(loop | 0x80); // Set the type of loop.
for (let i = initialN; i < n; i++) {
serialiser.setUint8(nametableObj[i]);
}
} else {
// The next 3 tiles are identical. Count how many of them in a row.
// It stops when it finds a different tile.
do {
loop++;
} while (initialTile === nametableObj[++n]);

serialiser.setUint8(loop);
serialiser.setUint8(initialTile);
}
}
};

export default serialiseRoomNametable;
130 changes: 130 additions & 0 deletions test/parseRoomNametable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import parseRoomNametable from '../src/lib/parser/room/parseRoomNametable.js';
import serialiseRoomNametable from '../src/lib/serialiser/room/serialiseRoomNametable.js';

const roomNametableEmptyBuffer = (tileset) => {
const buffer = new ArrayBuffer(1 + 16);
const view = new DataView(buffer);
view.setUint8(0x00, tileset); // Set the value of tileset.
return buffer;
};

const roomNametableBuffer = () => {
const array = [
0x01, 0x0d, 0x07, 0x27, 0x3c, 0x0d, 0x17, 0x2a, 0x3a, 0x0d, 0x0d, 0x2a,
0x28, 0x0d, 0x0d, 0x37, 0x20, 0x87, 0x01, 0x01, 0x62, 0x01, 0x63, 0x01,
0x64, 0x03, 0x65, 0x82, 0x63, 0x64, 0x04, 0x65, 0x82, 0x63, 0x64, 0x09,
0x65, 0x81, 0x63, 0x05, 0x65, 0x82, 0x63, 0x64, 0x03, 0x65, 0x83, 0x01,
0x63, 0x74, 0x05, 0x01, 0x81, 0x62, 0x05, 0x01, 0x84, 0x74, 0x01, 0x01,
0x74, 0x04, 0x01, 0x93, 0x01, 0x62, 0x62, 0x01, 0x63, 0x01, 0x64, 0x66,
0x67, 0x68, 0x63, 0x66, 0x67, 0x68, 0x65, 0x66, 0x63, 0x68, 0x69, 0x06,
0x6a, 0x8f, 0x6b, 0x66, 0x63, 0x68, 0x65, 0x66, 0x67, 0x68, 0x63, 0x66,
0x67, 0x68, 0x65, 0x01, 0x63, 0x0c, 0x01, 0x88, 0x62, 0x01, 0x01, 0x62,
0x01, 0x01, 0x62, 0x74, 0x04, 0x01, 0xa4, 0x63, 0x01, 0x64, 0x6c, 0x01,
0x6d, 0x63, 0x6c, 0x01, 0x6d, 0x65, 0x6c, 0x63, 0x6d, 0x6e, 0x36, 0x39,
0x37, 0x36, 0x39, 0x37, 0x6f, 0x6c, 0x63, 0x6d, 0x65, 0x6c, 0x01, 0x6d,
0x63, 0x6c, 0x01, 0x6d, 0x65, 0x01, 0x63, 0x03, 0x01, 0x87, 0x74, 0x01,
0x01, 0x62, 0x01, 0x01, 0x74, 0x04, 0x01, 0x86, 0x74, 0x01, 0x01, 0x74,
0x01, 0x01, 0x81, 0x62, 0x03, 0x01, 0xa5, 0x63, 0x01, 0x64, 0x70, 0x71,
0x72, 0x63, 0x70, 0x71, 0x72, 0x65, 0x70, 0x63, 0x72, 0x6e, 0x3e, 0x73,
0x40, 0x3b, 0x73, 0x3c, 0x6f, 0x70, 0x63, 0xb4, 0x65, 0x70, 0xb5, 0xb4,
0x63, 0x70, 0xb5, 0xb4, 0x65, 0x01, 0x63, 0x62, 0x13, 0x01, 0xa8, 0x01,
0x62, 0x01, 0x74, 0x63, 0x01, 0x64, 0x6c, 0x01, 0x6d, 0x63, 0x6c, 0x01,
0x6d, 0x65, 0x6c, 0x63, 0x6d, 0x6e, 0x3b, 0x3d, 0x40, 0x3b, 0x3d, 0x40,
0x6f, 0x6c, 0x63, 0x6d, 0x65, 0x6c, 0x01, 0x6d, 0x63, 0x6c, 0x01, 0x6d,
0x65, 0x01, 0x63, 0x03, 0x01, 0x83, 0x62, 0x01, 0x74, 0x03, 0x01, 0x81,
0x62, 0x04, 0x01, 0x83, 0x62, 0x01, 0x74, 0x03, 0x01, 0x04, 0x01, 0xa4,
0x63, 0x01, 0x64, 0x75, 0x76, 0x77, 0x63, 0x75, 0x76, 0x77, 0x65, 0x75,
0x63, 0x77, 0x6e, 0x3b, 0x3d, 0x40, 0x3b, 0x3d, 0x40, 0x78, 0x75, 0x63,
0x77, 0x65, 0x75, 0x76, 0x77, 0x63, 0x75, 0x76, 0x77, 0x65, 0x01, 0x63,
0x0b, 0x01, 0x81, 0x74, 0x08, 0x01, 0x87, 0x62, 0x01, 0x01, 0x79, 0x7a,
0x7b, 0x7c, 0x03, 0x7d, 0x82, 0x7a, 0x7c, 0x04, 0x7d, 0x8c, 0x7a, 0x64,
0x6e, 0x3b, 0x3d, 0x49, 0x49, 0x3d, 0x40, 0x6f, 0x65, 0x7a, 0x05, 0x7d,
0x81, 0x7a, 0x04, 0x7d, 0x83, 0x7b, 0x7a, 0xb6, 0x03, 0x01, 0x82, 0x74,
0x62, 0x03, 0x01, 0x81, 0x62, 0x08, 0x01, 0x82, 0x62, 0x01, 0xab, 0x01,
0x74, 0x01, 0x7e, 0x63, 0x01, 0x7f, 0x64, 0x7f, 0x64, 0x7f, 0x64, 0x7f,
0x64, 0x7f, 0x64, 0x7f, 0x64, 0x6e, 0x3b, 0x3d, 0x40, 0x3b, 0x3d, 0x40,
0x6f, 0x65, 0x7f, 0x64, 0x7f, 0x64, 0x7f, 0x64, 0x7f, 0x64, 0x7f, 0x64,
0x7f, 0x01, 0x63, 0x7e, 0x01, 0x74, 0x03, 0x01, 0x84, 0x74, 0x01, 0xb7,
0x62, 0x0a, 0xb7, 0x03, 0x01, 0xb4, 0x80, 0x63, 0x01, 0x81, 0x64, 0x81,
0x64, 0x81, 0x64, 0x81, 0x64, 0x81, 0x64, 0x81, 0x64, 0x6e, 0x3e, 0x3d,
0x40, 0x3b, 0x3d, 0x3c, 0x6f, 0x65, 0x81, 0x64, 0x81, 0x64, 0x81, 0x64,
0x81, 0x64, 0x81, 0x64, 0x81, 0x01, 0x63, 0x80, 0x01, 0x62, 0x01, 0x01,
0x62, 0x01, 0x01, 0xb8, 0xb9, 0xb8, 0xb8, 0xba, 0xb8, 0xb8, 0x05, 0xba,
0xb7, 0x62, 0x01, 0x01, 0x82, 0x82, 0x83, 0x82, 0x84, 0x82, 0x84, 0x82,
0x84, 0x82, 0x84, 0x82, 0x84, 0x82, 0x64, 0x6e, 0x41, 0x43, 0x44, 0x41,
0x43, 0x44, 0x6f, 0x65, 0x82, 0x84, 0xbb, 0x84, 0xbb, 0x84, 0xbb, 0x84,
0xbb, 0x84, 0xbb, 0x83, 0xbb, 0xbb, 0x01, 0xc4, 0xc5, 0x01, 0xc4, 0xc5,
0x01, 0xc4, 0xc5, 0xbc, 0xbc, 0xbd, 0xbc, 0xbc, 0x05, 0xbd, 0x84, 0x85,
0x86, 0x87, 0x01, 0x0d, 0x88, 0x82, 0x89, 0x84, 0x06, 0x8a, 0x83, 0x84,
0x8b, 0x88, 0x0c, 0xbe, 0x8a, 0x01, 0x01, 0xc7, 0xc7, 0x01, 0xc7, 0xc7,
0x01, 0xc7, 0xc7, 0x04, 0xbd, 0x82, 0xbf, 0xc0, 0x04, 0xbd, 0x84, 0x8c,
0x8d, 0x8e, 0x01, 0x08, 0x8f, 0x04, 0x90, 0x82, 0x91, 0x92, 0x08, 0x93,
0x82, 0x94, 0x91, 0x04, 0xce, 0x08, 0x8f, 0x8b, 0x01, 0x62, 0xbf, 0xc0,
0x01, 0xbf, 0xc0, 0x01, 0xbf, 0xc0, 0xbc, 0x05, 0xbd, 0x81, 0xbc, 0x03,
0xbd, 0x84, 0x01, 0x95, 0x01, 0x01, 0x06, 0x8f, 0x88, 0x96, 0x96, 0x97,
0x98, 0x97, 0x98, 0x99, 0x9a, 0x08, 0x9b, 0x86, 0x9c, 0x9d, 0x97, 0xcf,
0x97, 0xcf, 0x08, 0x8f, 0x14, 0xc1, 0x84, 0x9e, 0x9f, 0x9e, 0x9e, 0x08,
0x90, 0x85, 0xa0, 0xa1, 0xa0, 0xa1, 0x82, 0x0a, 0x84, 0x85, 0x82, 0xa0,
0xd1, 0xa0, 0xd1, 0x08, 0xc2, 0x14, 0x3d, 0x88, 0x3d, 0xa2, 0x3d, 0x3d,
0xa3, 0xa4, 0xa4, 0xa5, 0x16, 0x3d, 0x81, 0xc6, 0x1d, 0x3d, 0x88, 0x3d,
0xa2, 0x3d, 0x3d, 0xa6, 0xa7, 0xa8, 0xa7, 0x34, 0x3d,
];
const buffer = new ArrayBuffer(array.length);
const view = new DataView(buffer);
array.forEach((v, i) => view.setUint8(i, v));
return buffer;
};

describe('parseRoomNametable', () => {
it('should return a non empty object.', () => {
const { nametable } = parseRoomNametable(roomNametableEmptyBuffer(0));

assert.equal(typeof nametable, 'object');
assert.deepEqual(Object.keys(nametable), [
'tileset',
'palette',
'nametableObj',
]);
assert.ok(Number.isInteger(nametable.tileset));
assert.ok(Array.isArray(nametable.palette));
assert.equal(nametable.palette.length, 16);
assert.ok(Array.isArray(nametable.nametableObj));
assert.equal(nametable.nametableObj.length, 16);
assert.ok(Array.isArray(nametable.nametableObj[0]));
assert.equal(nametable.nametableObj[0].length, 64);
});

it('should parse the tileset.', () => {
const { nametable } = parseRoomNametable(roomNametableEmptyBuffer(5));

assert.equal(nametable.tileset, 5);
});

it('should return a map object.', () => {
const { nametableMap } = parseRoomNametable(roomNametableEmptyBuffer(0));

assert.equal(typeof nametableMap, 'object');
assert.equal(nametableMap.from, 0);
assert.equal(nametableMap.to, 16);
});

it('should return a map object with a start offset.', () => {
const { nametableMap } = parseRoomNametable(
roomNametableEmptyBuffer(0),
0xabc,
);

assert.equal(nametableMap.from, 0xabc);
assert.equal(nametableMap.to, 0xacc);
});

it('should be the inverse of serialiseRoomNametable.', () => {
const initialBuffer = roomNametableBuffer();
const { nametable } = parseRoomNametable(initialBuffer, 0, 60);
const buffer = serialiseRoomNametable(nametable);

assert.deepEqual(initialBuffer, buffer);
});
});
Loading

0 comments on commit 6d77cce

Please sign in to comment.