Skip to content

Instantly share code, notes, and snippets.

@savage69kr
Forked from eevee/pico8jstocart.py
Created June 16, 2022 06:47
Show Gist options
  • Save savage69kr/b16ed24dffcee9b42311123ccde9cbcb to your computer and use it in GitHub Desktop.
Save savage69kr/b16ed24dffcee9b42311123ccde9cbcb to your computer and use it in GitHub Desktop.
Python script to convert exported JavaScript back into a PICO-8 cartridge
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