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:

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
  1. 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')
  1. 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");
    // }
}
  1. 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');
  1. 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;
// }