Sunday, March 8, 2020

Multi-cart data storage on Pico-8

If you've played with Lexaloffle's Pico-8 for a little while, the limitations of the cart storage - not for graphics or sound, but for code and raw data (esp. tokens) - become a bottleneck very quickly.

Multiple cart support has been added to emulate a form of bank-switching, but it is implemented in a way that purposefully blocks your ability to write more code. The memory locations 0x4300 to around 0x6000 cannot be READ or WRITTEN - this is fairly illogical, because memory locations that cannot be either read or written can't really exist. 

You can, however, repurpose cartridge data to store byte data you create - you just have to know how to store it. The data in the cartridge is effectively hex strings in a specific order. Knowing this, we can write a quick tool to convert data we want to store into Pico-8's cartridge text format. 

We can then read it into the fairly large "user data" area of RAM at 0x4300 (in cartridge, this contains our code) and use it as we will. Loading takes a second, so you probably want to load in as much data as you can at once (i.e. entire towns, etc).

You can programatically store all sorts of data, and use your original cart as a sort of kernel. It will certainly be tricky, and games still won't be EXTREMELY complicated (as is the point of the engine), but having more storage is KEY to making complete games!

As a test, I wrote a text file (i.e. ascii-encoded string bytes) and, using a quick Python script, I converted it to a Pico-8 cart.

Pico-8 Cartridge Text Format:

pico-8 cartridge //
version 18
--Data stored here is inaccessible from the main cart.
--Use this area to describe the stored data instead.
--Data stored here begins at 0x0000 and goes to 0x1fff. 
--It is stored in .p8 as a BACKWARDS hex string, 128 chars by 128 rows.
--e.g. HELLO = 8454c4c4f4 
--Data stored here is from 0x3000 to 0x30ff.
--Its format is the same as the gfx section.
--Data here is 0x2000 to 0x2fff
--It is stored as a normal hex string, 256 chars by 32 rows.
--e.g. HELLO = 48454c4c4f

The three sections above will give you 12,543 bytes of storage per cart, less if you use them for actual graphics and maps. Multiply that by 15 possible storage banks gives you 1.8 megabytes of non-standard storage, and that doesn't include sfx and music!

As a note:
The __sfx__ and music blocks are less easy to make use of. A typical sfx test string looks like this within a .p8 file:
But when you peek the first 10 bytes of SFX ROM @ 0x3200, the values returned are:
63 10 63 10 63 10 63 10 63 10
3f corresponds to 63, then there are 3 characters in between (050) that equal 10 in decimal. Storing and retrieving data from a format like this may be too inefficient or impractical.

In Python, converting byte data to a hex string is fairly easy:

file = open("input.bin", 'rb') # Data to convert
by =               # Read all at once
file.close()                   # Close i/o stream
bstr = hex(by[0])              # First byte to hex string
byh = bstr[2]                  
byl = bstr[3]
outbyte = byl + byh            # Rearrange the characters

Iterate the above and paste it into a cart file - then by reading location 0x0000 of the new file (if located under __gfx__), you can convert to string data and print it:

The base cart just does this:

for i=0,250 do
 if c=='\\' then
 elseif c~=nil then

(chr() function is defined in the link above). The if block converts any backslash found in the data to a newline character. 

The peek and poke in the screenshot show that the string is actually living in user RAM.

My python tool is very messy (as mine always are!) but it will generate a full cartridge file, warn you if your input data is too large, and fill out all rows to the proper length. You can check out the source here.