UNPKG

p8-data-cart

Version:

Simple tools for generating Pico-8 data carts.

270 lines (269 loc) 8.58 kB
"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; }