Task 505: .P8 File Format
Task 505: .P8 File Format
The .P8 file format is the native text-based cartridge format for PICO-8, a fantasy console for game development. It is a human-readable text file that stores code, graphics, audio, and other data in delimited sections. The format is uncompressed and editable with any text editor. Specifications are based on official PICO-8 documentation and community wikis.
List of all properties intrinsic to the .P8 file format:
- Header: Two lines consisting of "pico-8 cartridge // http://www.pico-8.com" followed by "version X" (where X is an integer representing the PICO-8 version compatibility, e.g., 8; this can vary but the format structure remains consistent).
- Lua Section (lua): Plaintext Lua code (PICO-8 dialect), stored as lines of text until the next section delimiter. Represents the program's source code, with lowercase letters (uppercase converted on save) and UTF-8 encoded special glyphs.
- Graphics Section (gfx): Spritesheet data as 128 lines of 128 hexadecimal digits (0-f), each digit representing a 4-bit pixel color in the 16-color palette. Encodes a 128x128 pixel sheet (2 pixels per byte in memory, but serialized as hex in pixel order).
- Sprite Flags Section (gff): Sprite attributes as 2 lines of 256 hexadecimal digits each (512 digits total for 256 bytes), one byte per sprite (0-255). Each byte is a bitmask for 8 flags (LSB to MSB).
- Label Section (label): Cartridge label (screenshot) as 128 lines of 128 hexadecimal digits, similar to gfx but for a 128x128 image. Supports extended palette (g-v for colors 16-31).
- Map Section (map): Tilemap data as 32 lines of 256 hexadecimal digits each (128 bytes per line), encoding the top 128x32 tiles (sprite IDs 0-255 per tile). Bottom half shares space with gfx.
- SFX Section (sfx): Sound effects as 64 lines of 168 hexadecimal digits each (84 bytes per SFX). Structure per SFX: mode/filter (1 byte), duration (1 byte), loop start/end (2 bytes), 32 notes (80 bytes: pitch, waveform, volume, effect per note in nybbles).
- Music Section (music): Music patterns as 64 lines, each with 2 hex digits (flags byte for loop/start/stop), a space, and 8 hex digits (4 SFX IDs, one per channel; 0-63 or 0x41-0x44 for silence).
Two direct download links for .P8 files:
- https://raw.githubusercontent.com/erikyuzwa/snake-pico8/master/snake.p8
- https://raw.githubusercontent.com/silvanocerza/simple-snake/master/simple_snake.p8
Ghost blog embedded HTML JavaScript for drag-and-drop .P8 file dump (embed this in a Ghost blog post as custom HTML; it creates a drop zone, reads the file as text, parses sections based on delimiters, and displays all properties on screen):
Drag and drop a .P8 file here
- Python class for .P8 handling:
class P8File:
def __init__(self, filepath):
self.filepath = filepath
self.properties = {'header': [], 'lua': [], 'gfx': [], 'gff': [], 'label': [], 'map': [], 'sfx': [], 'music': []}
self.read()
def read(self):
with open(self.filepath, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
current_section = ''
for line in lines:
line = line.strip()
if line == '__lua__':
current_section = 'lua'
elif line == '__gfx__':
current_section = 'gfx'
elif line == '__gff__':
current_section = 'gff'
elif line == '__label__':
current_section = 'label'
elif line == '__map__':
current_section = 'map'
elif line == '__sfx__':
current_section = 'sfx'
elif line == '__music__':
current_section = 'music'
elif current_section:
self.properties[current_section].append(line)
else:
self.properties['header'].append(line)
def print_properties(self):
for key, value in self.properties.items():
if value:
print(f"{key.upper()}:")
print('\n'.join(value))
print('---')
def write(self, new_filepath=None):
filepath = new_filepath or self.filepath
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(self.properties['header']) + '\n')
for section, lines in self.properties.items():
if section != 'header' and lines:
f.write(f"__{section}__\n")
f.write('\n'.join(lines) + '\n')
# Example usage:
# p8 = P8File('example.p8')
# p8.print_properties()
# p8.properties['lua'].append('# new comment')
# p8.write('modified.p8')
- Java class for .P8 handling:
import java.io.*;
import java.util.*;
public class P8File {
private String filepath;
private Map<String, List<String>> properties = new HashMap<>();
public P8File(String filepath) {
this.filepath = filepath;
properties.put("header", new ArrayList<>());
properties.put("lua", new ArrayList<>());
properties.put("gfx", new ArrayList<>());
properties.put("gff", new ArrayList<>());
properties.put("label", new ArrayList<>());
properties.put("map", new ArrayList<>());
properties.put("sfx", new ArrayList<>());
properties.put("music", new ArrayList<>());
read();
}
public void read() {
try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
String line;
String currentSection = "";
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.equals("__lua__")) {
currentSection = "lua";
} else if (line.equals("__gfx__")) {
currentSection = "gfx";
} else if (line.equals("__gff__")) {
currentSection = "gff";
} else if (line.equals("__label__")) {
currentSection = "label";
} else if (line.equals("__map__")) {
currentSection = "map";
} else if (line.equals("__sfx__")) {
currentSection = "sfx";
} else if (line.equals("__music__")) {
currentSection = "music";
} else if (!currentSection.isEmpty()) {
properties.get(currentSection).add(line);
} else {
properties.get("header").add(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void printProperties() {
for (Map.Entry<String, List<String>> entry : properties.entrySet()) {
if (!entry.getValue().isEmpty()) {
System.out.println(entry.getKey().toUpperCase() + ":");
for (String val : entry.getValue()) {
System.out.println(val);
}
System.out.println("---");
}
}
}
public void write(String newFilepath) {
if (newFilepath == null) newFilepath = filepath;
try (PrintWriter pw = new PrintWriter(new File(newFilepath))) {
for (String headerLine : properties.get("header")) {
pw.println(headerLine);
}
for (Map.Entry<String, List<String>> entry : properties.entrySet()) {
if (!entry.getKey().equals("header") && !entry.getValue().isEmpty()) {
pw.println("__" + entry.getKey() + "__");
for (String line : entry.getValue()) {
pw.println(line);
}
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// Example usage:
// public static void main(String[] args) {
// P8File p8 = new P8File("example.p8");
// p8.printProperties();
// p8.properties.get("lua").add("-- new comment");
// p8.write("modified.p8");
// }
}
- JavaScript class for .P8 handling (node.js, using fs module):
const fs = require('fs');
class P8File {
constructor(filepath) {
this.filepath = filepath;
this.properties = {
header: [],
lua: [],
gfx: [],
gff: [],
label: [],
map: [],
sfx: [],
music: []
};
this.read();
}
read() {
const content = fs.readFileSync(this.filepath, 'utf8');
const lines = content.split('\n');
let currentSection = '';
lines.forEach((line) => {
line = line.trim();
if (line === '__lua__') {
currentSection = 'lua';
} else if (line === '__gfx__') {
currentSection = 'gfx';
} else if (line === '__gff__') {
currentSection = 'gff';
} else if (line === '__label__') {
currentSection = 'label';
} else if (line === '__map__') {
currentSection = 'map';
} else if (line === '__sfx__') {
currentSection = 'sfx';
} else if (line === '__music__') {
currentSection = 'music';
} else if (currentSection) {
this.properties[currentSection].push(line);
} else {
this.properties.header.push(line);
}
});
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
if (value.length > 0) {
console.log(`${key.toUpperCase()}:`);
console.log(value.join('\n'));
console.log('---');
}
}
}
write(newFilepath = this.filepath) {
let output = this.properties.header.join('\n') + '\n';
for (const [section, lines] of Object.entries(this.properties)) {
if (section !== 'header' && lines.length > 0) {
output += `__${section}__\n`;
output += lines.join('\n') + '\n';
}
}
fs.writeFileSync(newFilepath, output, 'utf8');
}
}
// Example usage:
// const p8 = new P8File('example.p8');
// p8.printProperties();
// p8.properties.lua.push('-- new comment');
// p8.write('modified.p8');
- C "class" (using struct and functions, as C doesn't have classes; for simplicity, assuming C99 or later):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LINES 10000 // Arbitrary max for simplicity
#define MAX_LINE_LEN 512
typedef struct {
char *filepath;
char **header;
int header_count;
char **lua;
int lua_count;
char **gfx;
int gfx_count;
char **gff;
int gff_count;
char **label;
int label_count;
char **map;
int map_count;
char **sfx;
int sfx_count;
char **music;
int music_count;
} P8File;
P8File* p8file_new(const char *filepath) {
P8File *p8 = malloc(sizeof(P8File));
p8->filepath = strdup(filepath);
p8->header = malloc(MAX_LINES * sizeof(char*));
p8->lua = malloc(MAX_LINES * sizeof(char*));
p8->gfx = malloc(MAX_LINES * sizeof(char*));
p8->gff = malloc(MAX_LINES * sizeof(char*));
p8->label = malloc(MAX_LINES * sizeof(char*));
p8->map = malloc(MAX_LINES * sizeof(char*));
p8->sfx = malloc(MAX_LINES * sizeof(char*));
p8->music = malloc(MAX_LINES * sizeof(char*));
p8->header_count = p8->lua_count = p8->gfx_count = p8->gff_count = 0;
p8->label_count = p8->map_count = p8->sfx_count = p8->music_count = 0;
return p8;
}
void p8file_read(P8File *p8) {
FILE *f = fopen(p8->filepath, "r");
if (!f) return;
char line[MAX_LINE_LEN];
char *current_section = NULL;
while (fgets(line, sizeof(line), f)) {
char *trimmed = strtok(line, "\r\n");
if (trimmed == NULL) trimmed = "";
char *dup = strdup(trimmed);
if (strcmp(dup, "__lua__") == 0) {
current_section = "lua";
} else if (strcmp(dup, "__gfx__") == 0) {
current_section = "gfx";
} else if (strcmp(dup, "__gff__") == 0) {
current_section = "gff";
} else if (strcmp(dup, "__label__") == 0) {
current_section = "label";
} else if (strcmp(dup, "__map__") == 0) {
current_section = "map";
} else if (strcmp(dup, "__sfx__") == 0) {
current_section = "sfx";
} else if (strcmp(dup, "__music__") == 0) {
current_section = "music";
} else if (current_section) {
if (strcmp(current_section, "lua") == 0) p8->lua[p8->lua_count++] = dup;
else if (strcmp(current_section, "gfx") == 0) p8->gfx[p8->gfx_count++] = dup;
else if (strcmp(current_section, "gff") == 0) p8->gff[p8->gff_count++] = dup;
else if (strcmp(current_section, "label") == 0) p8->label[p8->label_count++] = dup;
else if (strcmp(current_section, "map") == 0) p8->map[p8->map_count++] = dup;
else if (strcmp(current_section, "sfx") == 0) p8->sfx[p8->sfx_count++] = dup;
else if (strcmp(current_section, "music") == 0) p8->music[p8->music_count++] = dup;
} else {
p8->header[p8->header_count++] = dup;
}
}
fclose(f);
}
void p8file_print_properties(P8File *p8) {
if (p8->header_count > 0) {
printf("HEADER:\n");
for (int i = 0; i < p8->header_count; i++) printf("%s\n", p8->header[i]);
printf("---\n");
}
if (p8->lua_count > 0) {
printf("LUA:\n");
for (int i = 0; i < p8->lua_count; i++) printf("%s\n", p8->lua[i]);
printf("---\n");
}
// Similarly for other sections...
if (p8->gfx_count > 0) {
printf("GFX:\n");
for (int i = 0; i < p8->gfx_count; i++) printf("%s\n", p8->gfx[i]);
printf("---\n");
}
if (p8->gff_count > 0) {
printf("GFF:\n");
for (int i = 0; i < p8->gff_count; i++) printf("%s\n", p8->gff[i]);
printf("---\n");
}
if (p8->label_count > 0) {
printf("LABEL:\n");
for (int i = 0; i < p8->label_count; i++) printf("%s\n", p8->label[i]);
printf("---\n");
}
if (p8->map_count > 0) {
printf("MAP:\n");
for (int i = 0; i < p8->map_count; i++) printf("%s\n", p8->map[i]);
printf("---\n");
}
if (p8->sfx_count > 0) {
printf("SFX:\n");
for (int i = 0; i < p8->sfx_count; i++) printf("%s\n", p8->sfx[i]);
printf("---\n");
}
if (p8->music_count > 0) {
printf("MUSIC:\n");
for (int i = 0; i < p8->music_count; i++) printf("%s\n", p8->music[i]);
printf("---\n");
}
}
void p8file_write(P8File *p8, const char *new_filepath) {
const char *fp = new_filepath ? new_filepath : p8->filepath;
FILE *f = fopen(fp, "w");
if (!f) return;
for (int i = 0; i < p8->header_count; i++) fprintf(f, "%s\n", p8->header[i]);
if (p8->lua_count > 0) {
fprintf(f, "__lua__\n");
for (int i = 0; i < p8->lua_count; i++) fprintf(f, "%s\n", p8->lua[i]);
}
// Similarly for other sections...
if (p8->gfx_count > 0) {
fprintf(f, "__gfx__\n");
for (int i = 0; i < p8->gfx_count; i++) fprintf(f, "%s\n", p8->gfx[i]);
}
if (p8->gff_count > 0) {
fprintf(f, "__gff__\n");
for (int i = 0; i < p8->gff_count; i++) fprintf(f, "%s\n", p8->gff[i]);
}
if (p8->label_count > 0) {
fprintf(f, "__label__\n");
for (int i = 0; i < p8->label_count; i++) fprintf(f, "%s\n", p8->label[i]);
}
if (p8->map_count > 0) {
fprintf(f, "__map__\n");
for (int i = 0; i < p8->map_count; i++) fprintf(f, "%s\n", p8->map[i]);
}
if (p8->sfx_count > 0) {
fprintf(f, "__sfx__\n");
for (int i = 0; i < p8->sfx_count; i++) fprintf(f, "%s\n", p8->sfx[i]);
}
if (p8->music_count > 0) {
fprintf(f, "__music__\n");
for (int i = 0; i < p8->music_count; i++) fprintf(f, "%s\n", p8->music[i]);
}
fclose(f);
}
void p8file_free(P8File *p8) {
// Free all allocated lines and arrays
for (int i = 0; i < p8->header_count; i++) free(p8->header[i]);
free(p8->header);
// Similarly for other sections...
free(p8->lua);
free(p8->gfx);
free(p8->gff);
free(p8->label);
free(p8->map);
free(p8->sfx);
free(p8->music);
free(p8->filepath);
free(p8);
}
// Example usage:
// int main() {
// P8File *p8 = p8file_new("example.p8");
// p8file_read(p8);
// p8file_print_properties(p8);
// // Add to lua: p8->lua[p8->lua_count++] = strdup("-- new comment");
// p8file_write(p8, "modified.p8");
// p8file_free(p8);
// return 0;
// }