-
-
Save ReplayCoding/23aa37e1cc5883789a28322f7282fad5 to your computer and use it in GitHub Desktop.
Python script to convert exported JavaScript back into a PICO-8 cartridge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os.path | |
import re | |
import sys | |
# LZ-ish decompression scheme borrowed from picolove: | |
# https://github.com/gamax92/picolove/blob/master/cart.lua | |
compression_map = b"\n 0123456789abcdefghijklmnopqrstuvwxyz!#%(){}[]<>+=/*:;.,~_" | |
def decompress(code): | |
lua = bytearray() | |
mode = 0 | |
copy = None | |
i = 7 | |
codelen = (code[4] << 8) | code[5] | |
while len(lua) < codelen: | |
i = i + 1 | |
byte = code[i] | |
if mode == 1: | |
lua.append(byte) | |
mode = 0 | |
elif mode == 2: | |
# copy from buffer | |
offset = len(lua) - ((copy - 0x3c) * 16 + (byte & 0xf)) | |
length = (byte >> 4) + 2 | |
lua.extend(lua[offset:offset + length]) | |
mode = 0 | |
elif byte == 0x00: | |
# output next byte | |
mode = 1 | |
elif 0x01 <= byte <= 0x3b: | |
# output this byte from map | |
lua.append(compression_map[byte - 1]) | |
elif byte >= 0x3c: | |
# copy previous bytes | |
mode = 2 | |
copy = byte | |
return bytes(lua) | |
def main(): | |
if len(sys.argv) != 3: | |
print("usage: pico8jstocart.py infile.js outfile.p8") | |
sys.exit(1) | |
infile, outfile = sys.argv[1:] | |
if os.path.exists(outfile): | |
print("Cowardly refusing to overwrite", outfile) | |
sys.exit(1) | |
with open(infile) as f: | |
jsblob = f.read() | |
m = re.search(r'\bvar _cartdat=[[]([0-9,\n]+)[]]', jsblob) | |
if m: | |
jsdata = m.group(1) | |
else: | |
raise ValueError("Can't find _cartdat") | |
data = bytes(int(number) for number in jsdata.split(',')) | |
with open(outfile, 'w', encoding='latin1') as f: | |
def write(*args): | |
print(*args, file=f) | |
write("pico-8 cartridge // http://www.pico-8.com") | |
write("version 8") | |
# ROM layout largely matches the documented RAM layout: | |
# 0x0000 sprites | |
# 0x1000 shared sprite/map region | |
# 0x2000 map | |
# 0x3000 sprite flags | |
# 0x3100 music | |
# 0x3200 sounds | |
# 0x4300 Lua | |
# Lua is first in the cartridge, last in ROM | |
lua = data[67 * 256:] | |
if lua[:4] == b':c:\x00': | |
lua = decompress(lua) | |
lua = lua.decode('latin1') | |
write("__lua__") | |
write(lua) | |
# Next is the spritesheet. Somewhat inconveniently, the nybbles need | |
# flipping, because little-endian. | |
write("__gfx__") | |
for i in range(128): | |
write(''.join(data[j:j+1].hex()[::-1] for j in range(i * 64, (i + 1) * 64))) | |
# Sprite flags can be written out pretty much verbatim | |
write("__gff__") | |
write(data[0x3000:0x3080].hex()) | |
write(data[0x3080:0x3100].hex()) | |
# Map too! | |
write("__map__") | |
for offset in range(0x2000, 0x3000, 128): | |
write(data[offset:offset+128].hex()) | |
# Sound is more of a pain. It's stored in the ROM as a compact 4 nybbles | |
# per note, but the cartridge uses an expanded format with 5 nybbles per | |
# note. | |
write("__sfx__") | |
for offset in range(0x3200, 0x4300, 68): | |
sfxdata = data[offset:offset+64] | |
# Flags are at the end of the record in the ROM, but the beginning in | |
# the cart. Luckily they can be dumped as-is. | |
flags = data[offset+64:offset+68] | |
buf = [flags.hex()] | |
for n in range(0, len(sfxdata), 2): | |
b0, b1 = sfxdata[n:n+2] | |
note = b0 & 0x3f | |
props = (b1 << 2) | (b0 >> 6) | |
instrument = (props >> 0) & 0x7 | |
volume = (props >> 3) & 0x7 | |
effect = (props >> 6) & 0x7 | |
buf.append(f"{note:02x}{instrument:01x}{volume:01x}{effect:01x}") | |
write(''.join(buf)) | |
# Music also comes in a compressed form, but a rather simpler one | |
write("__music__") | |
for offset in range(0x3100, 0x3200, 4): | |
tracks = bytearray(data[offset:offset+4]) | |
flags = 0 | |
for t, track in enumerate(tracks): | |
if track & 0x80: | |
flags |= 1 << t | |
tracks[t] ^= 0x80 | |
write(f"{flags:02x}", tracks.hex()) | |
# And finally, the cart ends with a blank line. And done! | |
write() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment