Warning, this post is entirely about some technical minutia (serialization in C) and has no fun animated gifs.
I'm getting into track-data handling now...conveniently, I had already written a serializer for C data for my first Playdate project (a music authoring tool that I made for my sister and her boyfriend - they bought me the device as a birthday gift, and they're music-people who also have a Playdate). The serializer is based on ideas from this wonderful write-up about how Media Molecule handles serialization (for games like Little Big Planet and Dreams).
The original version was writing human-readable files, but this time I have a bigger pressure to minify the files because I want to let people share tracks as URLs. I know that pico8-edu limits URL-based sharing to around 2,000 bytes, and I just sort of assume that zep picked that number for a good reason...lol. Maybe something about URL size limits in popular social media sites.
Anyway! To get smaller files, I added a flag to the serializer for ASCII-mode/binary-mode, and this game will use the binary mode, unless I'm debugging something and want to inspect the file more easily in ASCII mode. Some programmers who are reading this are now wringing their hands together, thinking about how I'm going to write a bug where the two versions of the format have mismatched behavior in some sneaky place...and to be honest, you're probably right, but also, yolo.
Here's a very simple track (only three track-pieces, and a short replay) in ASCII format:
The first line is the magic prefix TRAQUE
(to make sure we're reading the expected type of file), then a byte "1" to say that we're in ASCII mode, and then a version number (this means that whenever I update the format, I can make things backwards-compatible). After that, we get the number of track-pieces, and then an entry for each piece. Finally, it stores a replay, which is a frame-count and then the raw bytes (converted to hex) of the replay keyframes (each keyframe is two bytes: first byte is "which buttons are you holding," second byte is "how many simulation-ticks did you hold this button-combo for"). This ASCII file is close to 350 bytes, which is concerningly large for such a barebones track. "Why don't you just zip it," you ask - doing so only takes it down to 300 bytes, so unfortunately it's not a huge win.
Next, here's the binary-version of a similar track:
I could show this in a hex editor, but it'd be equally meaningless, so I'm showing this screenshot because it looks funnier. ACK ACK!

You can see the same TRAQUE
prefix as before, and then the rest is illegible because it's just binary data, but it contains the same type of info as the ASCII version (version number, track pieces, a replay). This version is only about 60 bytes - much better!
The usage code for the serializer is reasonably comfy - here's the function which serializes a track-piece. As described in the Media Molecule page, this same function is used for both reading and writing - the serializer stores a flag for "am I reading or writing right now," which helps to avoid a lot of annoying and bug-prone near-duplicate code.
SerializeBlockStart()
and SerializeBlockEnd()
write curly-braces into the file, and also change the indent-level (but only in ASCII mode - in binary mode, these functions do nothing). SerializeInt()
takes a label (also only used in ASCII-mode), and a pointer to the int value that we're serializing. It's a pointer because the serializer might copy it or overwrite it, depending on whether it's currently reading or writing a file.
A more elaborate (but still pretty straightforward) example is serializing a replay:
Note that in this situation, we have to manually check if we're reading or writing - if we're reading, we need to reallocate replay->frames
(a pointer to a list of keyframes) with a size based on the keyframe-count that we just read. SerializeBytes()
can then serialize a pointer to a string of bytes with a given count.
The next thing to do is to actually load this data back into the game, which will be a fun milestone!