p8-data-cart
Version:
Simple tools for generating Pico-8 data carts.
270 lines (269 loc) • 8.58 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCart = parseCart;
const cart_data_js_1 = require("./cart-data.js");
const sfx_js_1 = require("./sfx.js");
const music_js_1 = require("./music.js");
class StringReader {
#string;
#index = 0;
get index() { return this.#index; }
constructor(string) {
this.#string = string;
}
isAtEnd() {
return this.#index == this.#string.length;
}
readChar() {
if (this.#index == this.#string.length)
return '';
const start = this.#index;
const end = this.#index + 1;
this.#index++;
return this.#string.substring(start, end);
}
skip(distance = 1) {
this.#index = Math.min(this.#index + distance, this.#string.length);
}
peek(length = 1) {
const start = this.#index;
const end = this.#index + length;
return this.#string.substring(start, end);
}
}
function parseExtendedHex(char) {
switch (char) {
case '0': return 0x0;
case '1': return 0x1;
case '2': return 0x2;
case '3': return 0x3;
case '4': return 0x4;
case '5': return 0x5;
case '6': return 0x6;
case '7': return 0x7;
case '8': return 0x8;
case '9': return 0x9;
case 'a': return 0xa;
case 'b': return 0xb;
case 'c': return 0xc;
case 'd': return 0xd;
case 'e': return 0xe;
case 'f': return 0xf;
case 'g': return 0x0 + 0x80;
case 'h': return 0x1 + 0x80;
case 'i': return 0x2 + 0x80;
case 'j': return 0x3 + 0x80;
case 'k': return 0x4 + 0x80;
case 'l': return 0x5 + 0x80;
case 'm': return 0x6 + 0x80;
case 'n': return 0x7 + 0x80;
case 'o': return 0x8 + 0x80;
case 'p': return 0x9 + 0x80;
case 'q': return 0xa + 0x80;
case 'r': return 0xb + 0x80;
case 's': return 0xc + 0x80;
case 't': return 0xd + 0x80;
case 'u': return 0xe + 0x80;
case 'v': return 0xf + 0x80;
default: return 0x00;
}
}
function isDecimal(char) {
return /[0-9]/.test(char);
}
function isHexadecimal(char) {
return /[0-9a-fA-F]/.test(char);
}
function isExtendedHexadecimal(char) {
return /[0-9a-vA-V]/.test(char);
}
function isWhitespace(char) {
return /^\s$/.test(char);
}
function isAlphanumeric(char) {
return /^[0-9a-zA-Z]$/.test(char);
}
function expect(reader, expected, contextMessage) {
const actual = reader.peek(expected.length);
if (actual !== expected) {
let errorMessage;
if (contextMessage !== undefined)
errorMessage = `Expected ${expected}, but found ${actual}`;
else
errorMessage = `${contextMessage}: Expected ${expected}, but found ${actual}`;
throw new Error(errorMessage);
}
}
function expectHexadecimal(reader) {
const nextChar = reader.peek();
if (!isHexadecimal(nextChar))
throw new Error(`Expected a hexadecimal number, but found "${nextChar}"`);
}
function expectExtendedHexadecimal(reader) {
const nextChar = reader.peek();
if (!isExtendedHexadecimal(nextChar))
throw new Error(`Expected an extended hexadecimal number, but found "${nextChar}"`);
}
function expectDecimal(reader) {
const nextChar = reader.peek();
if (!isDecimal(nextChar))
throw new Error(`Expected a decimal number, but found "${nextChar}"`);
}
function skipVerbatim(reader, expected, contextMessage) {
expect(reader, expected, contextMessage);
reader.skip(expected.length);
}
function skipWhitespace(reader) {
while (isWhitespace(reader.peek())) {
reader.skip();
}
}
function skipHeader(reader) {
skipVerbatim(reader, 'pico-8 cartridge // http://www.pico-8.com\n', 'Unknown header');
}
function readVersion(reader) {
skipVerbatim(reader, 'version ');
return readDecimal(reader);
}
function readSectionLabel(reader) {
skipVerbatim(reader, '__', 'Section label start');
let sectionName = '';
while (isAlphanumeric(reader.peek())) {
sectionName += reader.readChar();
}
skipVerbatim(reader, '__', 'Section label end');
return `__${sectionName}__`;
}
function transposeByte(byte) {
return (byte & 0x0f) << 4 | (byte & 0xf0) >> 4;
}
function readDataSection(reader, transposeBytes = false) {
let bytes = [];
do {
while (isHexadecimal(reader.peek()) && !reader.isAtEnd()) {
const value = readHexadecimal(reader, 2);
bytes.push(transposeBytes ? transposeByte(value) : value);
}
skipWhitespace(reader);
} while (isHexadecimal(reader.peek()));
return new Uint8Array(bytes);
}
function newSectionEncountered(reader) {
const str = reader.peek(10);
return /^\n__(lua|gfx|gff|label|map|sfx|music)__(\s|$)/.test(str);
}
function readLuaSection(reader) {
let luaCode = '';
while (!reader.isAtEnd()) {
luaCode += reader.readChar();
if (reader.peek(1) === '\n') {
if (newSectionEncountered(reader))
break;
}
}
skipWhitespace(reader);
return luaCode;
}
function readGfxSection(reader) {
return readDataSection(reader, true);
}
function readLabelSection(reader) {
let bytes = [];
do {
while (isExtendedHexadecimal(reader.peek()) && !reader.isAtEnd()) {
bytes.push(readExtendedHexadecimal(reader, 1));
}
skipWhitespace(reader);
} while (isExtendedHexadecimal(reader.peek()));
return new Uint8Array(bytes);
}
function readGffSection(reader) {
return readDataSection(reader);
}
function readMapSection(reader) {
return readDataSection(reader);
}
function readSfxSection(reader) {
const cartSfxBytes = readDataSection(reader);
const sfxData = (0, sfx_js_1.cartBytesToSfxData)(cartSfxBytes);
return (0, sfx_js_1.sfxDataToRuntimeBytes)(sfxData);
}
function readMusicSection(reader) {
let bytes = [];
do {
bytes.push(readHexadecimal(reader, 2));
skipVerbatim(reader, ' ');
bytes.push(readHexadecimal(reader, 2));
bytes.push(readHexadecimal(reader, 2));
bytes.push(readHexadecimal(reader, 2));
bytes.push(readHexadecimal(reader, 2));
skipWhitespace(reader);
} while (isHexadecimal(reader.peek()));
skipWhitespace(reader);
const cartMusicBytes = new Uint8Array(bytes);
const musicData = (0, music_js_1.cartBytesToMusicData)(cartMusicBytes);
return (0, music_js_1.musicDataToRuntimeBytes)(musicData);
}
function readDecimal(reader) {
expectDecimal(reader);
let numString = '';
while (isDecimal(reader.peek())) {
numString += reader.readChar();
}
return parseInt(numString);
}
function readHexadecimal(reader, numberOfDigits) {
let value = 0;
for (let i = 0; i < numberOfDigits; i++) {
expectHexadecimal(reader);
const hexChar = reader.readChar();
value *= 0x10;
value += parseExtendedHex(hexChar);
}
return value;
}
function readExtendedHexadecimal(reader, numberOfDigits) {
let value = 0;
for (let i = 0; i < numberOfDigits; i++) {
expectExtendedHexadecimal(reader);
const hexChar = reader.readChar();
value *= 0x10;
value += parseExtendedHex(hexChar);
}
return value;
}
function parseCart(cartString) {
const reader = new StringReader(cartString);
const cartData = new cart_data_js_1.CartData();
skipHeader(reader);
cartData.version = readVersion(reader);
skipWhitespace(reader);
while (!reader.isAtEnd()) {
const sectionLabel = readSectionLabel(reader);
skipWhitespace(reader);
switch (sectionLabel) {
case '__lua__':
cartData.lua = readLuaSection(reader);
break;
case '__gfx__':
cartData.gfx.set(readGfxSection(reader));
break;
case '__label__':
cartData.label.set(readLabelSection(reader));
break;
case '__gff__':
cartData.gff.set(readGffSection(reader));
break;
case '__map__':
cartData.map.set(readMapSection(reader));
break;
case '__sfx__':
cartData.sfx.set(readSfxSection(reader));
break;
case '__music__':
cartData.music.set(readMusicSection(reader));
break;
}
}
return cartData;
}