main dff24945249a cached
22 files
94.0 KB
28.2k tokens
18 symbols
1 requests
Download .txt
Repository: jaames/playdate-reverse-engineering
Branch: main
Commit: dff24945249a
Files: 22
Total size: 94.0 KB

Directory structure:
gitextract_ijazx7lw/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── formats/
│   ├── fnt.md
│   ├── luac.md
│   ├── pda.md
│   ├── pdex.md
│   ├── pdi.md
│   ├── pds.md
│   ├── pdt.md
│   ├── pdv.md
│   ├── pdz.md
│   └── pft.md
├── readme.md
├── server/
│   └── api.md
├── tools/
│   ├── pdex2elf.py
│   ├── pdi2png.py
│   ├── pdz.py
│   └── usbeval.py
└── usb/
    ├── stream.md
    ├── usb.md
    └── usb_unlocked.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: jaames


================================================
FILE: .gitignore
================================================
testing
tools/*lua
.obsidian

================================================
FILE: LICENSE
================================================
Creative Commons Legal Code

CC0 1.0 Universal

    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
    HEREUNDER.

Statement of Purpose

The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").

Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.

For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.

1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:

  i. the right to reproduce, adapt, distribute, perform, display,
     communicate, and translate a Work;
 ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
     likeness depicted in a Work;
 iv. rights protecting against unfair competition in regards to a Work,
     subject to the limitations in paragraph 4(a), below;
  v. rights protecting the extraction, dissemination, use and reuse of data
     in a Work;
 vi. database rights (such as those arising under Directive 96/9/EC of the
     European Parliament and of the Council of 11 March 1996 on the legal
     protection of databases, and under any national implementation
     thereof, including any amended or successor version of such
     directive); and
vii. other similar, equivalent or corresponding rights throughout the
     world based on applicable law or treaty, and any national
     implementations thereof.

2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.

3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.

4. Limitations and Disclaimers.

 a. No trademark or patent rights held by Affirmer are waived, abandoned,
    surrendered, licensed or otherwise affected by this document.
 b. Affirmer offers the Work as-is and makes no representations or
    warranties of any kind concerning the Work, express, implied,
    statutory or otherwise, including without limitation warranties of
    title, merchantability, fitness for a particular purpose, non
    infringement, or the absence of latent or other defects, accuracy, or
    the present or absence of errors, whether or not discoverable, all to
    the greatest extent permissible under applicable law.
 c. Affirmer disclaims responsibility for clearing rights of other persons
    that may apply to the Work or any use thereof, including without
    limitation any person's Copyright and Related Rights in the Work.
    Further, Affirmer disclaims responsibility for obtaining any necessary
    consents, permissions or other rights required for any use of the
    Work.
 d. Affirmer understands and acknowledges that Creative Commons is not a
    party to this document and has no duty or obligation with respect to
    this CC0 or use of the Work.


================================================
FILE: formats/fnt.md
================================================
A file with the `.fnt` extension contains font data created by [playdate caps](https://play.date/caps/). It is a line oriented UTF-8 plaintext file format, suitable for editing with normal text editors and optionally includes a base64 encoded PNG sprite sheet.

Each line is one of:

1. String key value data separated by an equals character `=`
2. Glyph widths: A UTF-8 character, whitespace and pixel width of the character
3. Kerning data: Two UTF-8 characters, whitespace and pixel offset for this kerning pair.
4. Lua style comments beginning with `--` and empty lines which are skipped


## Glyph Widths

````
8   8
9   8
space   5
�   9
````

In its most basic form a fnt file is one or more lines comprising an index to specify
characters and widths for each glyphs in an accompanying PNG sprite sheet.
Pairs of UTF-8 glyphs and their widths are separated by any amount of whitespace,
one per line, specified in the order they appear in the sprite sheet.
(Left to Right, Top to Buttom)

Playdate supports all code points in the first four Unicode planes, up to U+3FFFF.

Because these glyphs and widths are whitespace separated, a special string
 `space` is substited for the ` ` space glyph.

## External PNG Data

The PNG may be external file or included internally in the fnt file.
When external, the accompanying PNG must be named to match the font file.
For example the PNG for `pantspants.fnt` would named pantspants-table-9-12.png
assuming the fonts glyphs are 9 pixels wide and 12 pixels tall.

## Internal PNG Data

Alternatively the PNG data may be included within the fnt file itself, base64 encoded
along with the necessary metadata (height/width) required to process the PNG sprite sheet.

```
datalen=2052
data=iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAAAXNSR0IArs4c6QAAAF5JREFUOE+tktsKACAIQ/X/P7owGHjJUMm3pJ02k2lei4jYy0OjyBcYyjAmQO/MnLtAOBMdQLoXZ/CIrGPquCb+1KEA4dLMsgsUceb0gCdAQPUc719eXBlc+7qH6dsbK4QPCz6OhZ4AAAAASUVORK5CYII=
```
| Key        | Value Detail |
|:-----------|:-----------------------------------------|
| `datalen=` | Integer length of `data` data value which follows (as ASCII numbers)
| `data=`    | PNG file data, Base64 encoded
| `width=`   | Maximum pixel width of each glyph
| `height=`  | Maximum pixel height of each glyph

A 1bit B+W PNG image contains a fixed-size sprite sheet of the individual glyphs.
If the glyphs themselves are narrower than the width their pixels are left justified.


## Kerning Pairs (optional)

```
To      -2
Te      -4
```

Kerning pairs may be specified, one line per pair with: two characters, whitespace and the offset.

## Tracking info (optional)

| Key        | Value Detail |
|:-----------|:-----------------------------------------|
| `tracking=`| Number of pixels of horizontal whitespace between glyphs within a string. Defaults to 1


## CAPS Metadata (optional)

```
--metrics={"baseline":10,"xHeight":0,"capHeight":0,"pairs":{"Te":[-6,0]},"left":[],"right":[]}```
```

The metrics line embeds a JSON object in a lua-style comment to store relevant CAPS editor metadata.
This data is ignored by `pdc`.
The [baseline](https://en.wikipedia.org/wiki/Baseline_(typography)),
[xHeight](https://en.wikipedia.org/wiki/X-height) and
[capHeight](https://en.wikipedia.org/wiki/Cap_height)
are displayed while using the Caps Glyph editor, `left` and `right` contain an array of strings where
each string includes a list of characters which have equivalent left or right-most columns and thus
will use identical kerning information.  The pairs object includes kerning pairs specified by the
Caps "Auto-Kern" functionality.


================================================
FILE: formats/luac.md
================================================
Lua-based Playdate games use a tweaked version of Lua 5.4.3. You will only find compiled Lua bytecode in [`.pdz`](/formats/pdz.md) files.

### Compile Flags

`#define LUA_32BITS = 1` set in `luaconf.h`.

### Header Differences

TODO

### Opcode Differences

To maintain backwards-compatibility with Lua 5.4-beta bytecode from [pre-release versions of the Playdate SDK](#pre-sdk-version-180), additional Lua 5.4.3 opcodes were appended to the opcode map like so:

```
LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       MM OT IT T  A  mode		   opcode  */
  opmode(0, 0, 0, 0, 1, iABC)		/* OP_MOVE */
 ,opmode(0, 0, 0, 0, 1, iAsBx)		/* OP_LOADI */
 ,opmode(0, 0, 0, 0, 1, iAsBx)		/* OP_LOADF */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_LOADK */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_LOADKX */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_UNKNOWN */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_LOADNIL */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_GETUPVAL */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_SETUPVAL */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_GETTABUP */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_GETTABLE */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_GETI */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_GETFIELD */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_SETTABUP */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_SETTABLE */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_SETI */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_SETFIELD */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_NEWTABLE */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SELF */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_ADDI */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_ADDK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SUBK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_MULK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_MODK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_POWK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_DIVK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_IDIVK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BANDK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BORK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BXORK */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SHRI */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SHLI */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_ADD */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SUB */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_MUL */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_MOD */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_POW */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_DIV */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_IDIV */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BAND */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BOR */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BXOR */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SHL */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_SHR */
 ,opmode(1, 0, 0, 0, 0, iABC)		/* OP_MMBIN */
 ,opmode(1, 0, 0, 0, 0, iABC)		/* OP_MMBINI*/
 ,opmode(1, 0, 0, 0, 0, iABC)		/* OP_MMBINK*/
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_UNM */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_BNOT */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_NOT */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_LEN */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_CONCAT */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_CLOSE */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_TBC */
 ,opmode(0, 0, 0, 0, 0, isJ)		/* OP_JMP */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_EQ */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_LT */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_LE */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_EQK */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_EQI */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_LTI */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_LEI */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_GTI */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_GEI */
 ,opmode(0, 0, 0, 1, 0, iABC)		/* OP_TEST */
 ,opmode(0, 0, 0, 1, 1, iABC)		/* OP_TESTSET */
 ,opmode(0, 1, 1, 0, 1, iABC)		/* OP_CALL */
 ,opmode(0, 1, 1, 0, 1, iABC)		/* OP_TAILCALL */
 ,opmode(0, 0, 1, 0, 0, iABC)		/* OP_RETURN */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_RETURN0 */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_RETURN1 */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_FORLOOP */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_FORPREP */
 ,opmode(0, 0, 0, 0, 0, iABx)		/* OP_TFORPREP */
 ,opmode(0, 0, 0, 0, 0, iABC)		/* OP_TFORCALL */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_TFORLOOP */
 ,opmode(0, 0, 1, 0, 0, iABC)		/* OP_SETLIST */
 ,opmode(0, 0, 0, 0, 1, iABx)		/* OP_CLOSURE */
 ,opmode(0, 1, 0, 0, 1, iABC)		/* OP_VARARG */
 ,opmode(0, 0, 1, 0, 1, iABC)		/* OP_VARARGPREP */
 ,opmode(0, 0, 0, 0, 0, iAx)		/* OP_EXTRAARG */

 // opcodes appended:

 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_LOADFALSE */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_LFALSESKIP */
 ,opmode(0, 0, 0, 0, 1, iABC)		/* OP_LOADTRUE */
};
```

## Pre SDK Version 1.8.0

Prior to SDK version 1.8.0 (which was only available to developer preview members - 1.9.0 was the first publicly available SDK release), the Playdate Lua runtime was based on [prerelease/beta version of Lua 5.4](https://github.com/lua/lua/tree/6c0e44464b9eef4be42e2c8181aabfb3301617ad). This version used a slightly nonstandard bytecode header structure.

It is possible to execute Playdate Lua bytecode from this version of the SDK by compiling the Lua 5.4-beta with `#define LUA_32BITS = 1` set in `luaconf.h`.

### Header

| Offset | Type    | Detail |
|:-------|:--------|:-------|
| `0`    | `byte[4]` | Constant `LUA_SIGNATURE` (hex `1B 4C 75 61`) |
| `4`    | `uint16`  | Version (`0x03F8` = 5.4.0 prerelease) |
| `6`    | `byte`    | Constant `LUAC_FORMAT` (hex `00`) |
| `7`    | `byte[6]` | Constant `LUAC_DATA` (hex `19 93 0D 0A 1A 0A`) |
| `13`   | `uint8`   | Instruction size (always `4`) |
| `14`   | `uint8`   | Integer size (always `4`) |
| `15`   | `uint8`   | Number size (always `4`) |
| `16`   | `lua int`  | Constant `LUA_INT` (`0x5678`) |
| `20`   | `lua float`  | Constant `LUA_NUM` (`370.5`) |

================================================
FILE: formats/pda.md
================================================
A file with the `.pda` extension represents audio data that has been compiled by `pdc`. This format uses little endian byte order.

## Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `char[12]` | Ident "Playdate AUD" |
| `12`   | `uint24`  | Sample rate (in Hz) |
| `15`   | `uint8`  | [Audio data format](#audio-data-format) |

### Audio Data Format

The audio data format field in the file header seems to map to the `playdate.sound` constants in the official SDK:

| Value | SDK Constant | Detail |
|:------|:-------------|:-------|
| `0`   | `kFormat8bitMono` | unsigned 8-bit PCM, one channel |
| `1`   | `kFormat8bitStereo` | unsigned 8-bit PCM, two channels |
| `2`   | `kFormat16bitMono` | signed 16-bit little endian PCM, one channel |
| `3`   | `kFormat16bitStereo` | signed 16-bit little endian PCM, two channels |
| `4`   | `kFormatADPCMMono` | 4-bit IMA ADPCM, one channel |
| `5`   | `kFormatADPCMStereo` | 4-bit IMA ADPCM, two channels |

## Audio Data

The format flag in the file header indicates how the audio is stored:

### 4-bit IMA ADPCM

In this format, the audio is encoded in blocks. The audio data begins with an `uint16` which gives the size of a single block, and the start of the first block begins immediately after.

Each block also begins with a small header consisting of 4 bytes for each audio channel:

| Type   | Detail |
|:-------|:-------|
| `uint16` | ADPCM predictor |
| `uint8` | ADPCM step index |
| `uint8` | Reserved, should always be zero |

The rest of the block contains regular 4-bit IMA ADPCM audio samples. For stereo audio, the left channel uses the high nibble of every byte, while the right channel uses the low nibble.

### 8-bit PCM

Standard 8-bit PCM. Each sample can be converted to signed 16-bit PCM with `(sample - 0x80) << 8`. For stereo audio, the channels are interleaved so you read one sample for the left channel, the next one for the right, next one for the left, and so on.

### 16-bit PCM

Standard signed 16-bit PCM in little-endian byte order. As with the 8-bit format, for stereo audio, the channels are interleaved so you read one sample for the left channel, the next one for the right, next one for the left, and so on.


================================================
FILE: formats/pdex.md
================================================
The `pdex.bin` file represents information and executable code copied by `pdc` from a `pdex.elf` ELF file compiled for ARM32 (Thumb, EABI v5, hard-float). It is usually the entry point of Playdate games created using the C API. The file format uses little endian byte order.

Supplementary reading:

- https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
- https://www.man7.org/linux/man-pages/man5/elf.5.html

# File structure

1. [File header](#file-header)
2. [Program header](#program-header)
3. [Program data](#program-data)
   1. [Program segment](#program-segment)
   2. [Relocation entries](#relocation-entries)

## File header

| Offset | Type       | Detail                         |
|:-------|:-----------|:-------------------------------|
| `0x00` | `char[12]` | File signature: `Playdate PDX` |
| `0x0C` | `uint32`   | [Flags](#flags)                |
| `0x10` | -          | End of file header (size)      |

### Flags

| Bitmask             | Detail                                                |
|:--------------------|:------------------------------------------------------|
| `flag & 0x40000000` | If `> 0`, all data after the file header is encrypted |

Encryption is (at the time of writing) only used by Catalog games as a form of DRM. The encryption method is not yet known.

## Program header

| Offset | Type        | Detail                                             |
|:-------|:------------|:---------------------------------------------------|
| `0x00` | `uint8[16]` | MD5 checksum of program segment                    |
| `0x10` | `uint32`    | Size of program segment in file image; `p_filesz`  |
| `0x14` | `uint32`    | Size of program segment in memory image; `p_memsz` |
| `0x18` | `uint32`    | Entry point address; `e_entry`                     |
| `0x1C` | `uint32`    | Number of relocation entries                       |
| `0x20` | -           | End of program header (size)                       |

## Program data

The program data is zlib-compressed and consists of [a single program segment](#program-segment) immediately followed by [relocation entries](#relocation-entries).

### Program segment

The first `p_filesz` bytes of the uncompressed program data is the program segment, usually consisting of the `.text` (executable code) and `.data` (initialized global variables) sections of the original ELF file. The `.bss` (uninitialized global variables) section does not occupy any space in the file image; its size in memory can be computed via `p_memsz - p_filesz`.

### Relocation entries

The next `<number of relocation entries> * 4` bytes of the uncompressed program data are the relocation entries, usually from the `.rel.text` and/or `.rel.data` sections of the original ELF file. Each entry is a single `uint32` denoting a byte offset from the beginning of the program segment where a relocation should take place, corresponding to the `r_offset` member of an `Elf32_Rel` relocation entry.


================================================
FILE: formats/pdi.md
================================================
A file with the `.pdi` extension represents a 1-bit bitmap image that has been compiled by `pdc`. This format uses little endian byte order.

## Header

| Offset | Type      | Detail               |
|:-------|:----------|:---------------------|
| `0`    | `char[12]` | Ident `Playdate IMG` |
| `12`   | `uint32`   | [Flags](#flags) |

### Flags

| Bitmask             | Detail                                      |
|:--------------------|:--------------------------------------------|
| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |

## Image Header

If the compression flag is set, this image header follows the file header. Everything after the image header is zlib-compressed. 

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint32`  | Size of image data section when decompressed |
| `4`    | `uint32`  | Image width (in pixels) |
| `8`    | `uint32`  | Image height (in pixels) |
| `12`   | `uint32`  | Unknown/reserved? Seen as 0 |

## Image Data

`.pdi` image data comprises of a single [Image Cell](#image-cell).

## Image Cell

The `pdi`, [`.pdt`](formats/pdi.md) and [`.pft`](formats/pft.md) formats store pixels as "cells", where transparent edges are cropped out to save on space. 

### Cell Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `uint16` | Cell clip width (in pixels) |
| `2`    | `uint16` | Cell clip height (in pixels) |
| `4`    | `uint16` | Cell stride (bytes per image row) |
| `6`    | `uint16` | Cell clip left (in pixels) |
| `8`    | `uint16` | Cell clip right (in pixels) |
| `10`   | `uint16` | Cell clip top (in pixels) |
| `12`   | `uint16` | Cell clip bottom (in pixels) |
| `14`   | `uint16` | [Cell bitflags](#cell-bitflags) |

### Cell Bitflags

| Bitmask             | Detail                     |
|:--------------------|:---------------------------|
| `flags & 0x3` | If `> 0`, cell uses transparency |

### Cell Pixels

Cells contain at least one 1-bit bitmap for black/white color (`0` for black and `1` for white). If the transparency flag is set, this will be followed by an additional 1-bit bitmap for the image alpha (`0` for transparent and `1` for opaque).

The number of bytes used by a cell bitmap will be equal to `stride * clip height`. The number of pixels in each row of the cell bitmap will be equal to `clip width`. Transparent edges are not stored, and must be added back to the cell based on the values given in the cell header. The stride is always a multiple of 4, ensuring that the data can be accessed with 32-bit reads.

![Transparent edges are removed from the image to reduce its size](https://github.com/jaames/playdate-reverse-engineering/blob/main/_images/bitmap-clip.png)

The final image width will equal `clip left + clip width + clip right`, likewise the height will equal `clip top + clip height + clip bottom`,


================================================
FILE: formats/pds.md
================================================
A file with the `.pds` extension represents a collection localization strings that have been compiled by `pdc`. This format uses little endian byte order.

## Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `char[12]` | Ident "Playdate STR" |
| `12`   | `uint32`   | [Flags](#flags) |

### Flags

| Bitmask             | Detail                                      |
|:--------------------|:--------------------------------------------|
| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |

### String header

If the compression flag is set, there's an extra string data header after the file header. Everything after this is zlib-compressed. 

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`   | `uint32`  | Size of decompressed string data |
| `4`   | `uint32`  | Unused/reserved, seen as 0 |
| `8`   | `uint32`  | Unused/reserved, seen as 0 |
| `12`  | `uint32`  | Unused/reserved, seen as 0 |

## String Data

### Table Header

| Offset | Type    | Detail |
|:-------|:--------|:-------|
| `0`    | `uint32` | Number of string entries |

### Table

After this header, there is a table of int32 offsets for each string entry aside from the first one, as well as an offset to the end of the data.

Offsets are relative to the end of the table, and the first string entry always begins directly after the table.

### String Entries

Each string entry contains an utf8 string key, followed by a null byte, followed by a utf8 string value, followed by another null byte.

================================================
FILE: formats/pdt.md
================================================
A file with the `.pdt` extension represents a 1-bit bitmap image table containing multiple sub-images (like a spritesheet or animation) that has been compiled by `pdc`. This format uses little endian byte order.

## Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `char[12]` | Ident `Playdate IMT` |
| `12`   | `uint32`   | [Flags](#flags) |

### Flags

| Bitmask             | Detail                                      |
|:--------------------|:--------------------------------------------|
| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |

## Image Header

If the compression flag is set, there's an extra header after the file header:

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint32`  | Size of decompressed image data |
| `4`    | `uint32`  | Image width (in pixels) |
| `8`    | `uint32`  | Image height (in pixels) |
| `12`   | `uint32`  | Number of cells |

The image width and height are for the first image only. In sequential image tables, the following images may be of
different sizes. In matrix image tables, all images must be the same size.

If the compression flag is set, then this section is zlib-compressed.

## Image Data

### Table Header

| Offset | Type    | Detail |
|:-------|:--------|:-------|
| `0`    | `uint16` | Num cells |
| `2`    | `uint16` | Num cells per row |

For sequential image tables, the values will be the same. For matrix image tables, the second value will be the number of cells on each row.

### Table

After this header, there is a table of uint32 offsets for each [image cell](#image-cell) aside from the first one, as well as an offset to the end of the data.

Offsets are relative to the end of the table, and the first cell always begins directly after the table.

### Image Cell

See: [Image Cell](/formats/pdi.md#image-cell)


================================================
FILE: formats/pdv.md
================================================
A file with the `.pdv` extension represents a 1-bit video that has been converted by `1bitvideo.app`. This format uses little endian byte order.

## Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `char[12]` | Ident `Playdate VID` |
| `12`   | `uint32`   | Reserved, always 0  |
| `16`   | `uint16` | Number of frames |
| `18`   | `uint16` | Reserved, always 0 |
| `20`   | `float32` | Framerate, measured in frames per second |
| `24`   | `uint16` | Frame width (in pixels) |
| `26`   | `uint16` | Frame height (in pixels) |

In 1bitvideo.app the frame width and height seem to be hardcoded to `400` and `240` respectively, at least at the time of writing.

## Frame Table

Following the header is a series of `uint32` values, one for each frame, and one additional to mark the end of the data.  So if the number of frames is 16, there will be 17 entries in this table. These values contain the frame data offset as well as the frame type:

| Value | Detail |
|:------|:-------|
| `value >> 2` | Offset |
| `value & 0x3` | Frame type |

### Frame Types

| Type | Detail |
|:-----|:-------|
| `0`  | No frame |
| `1`  | [I-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |
| `2`  | [P-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |
| `3`  | Combined I-frame and P-frame |

A `0` type frame is placed at the end to identify where the preceeding frame's data ends. There is no actual data following it.

## Frame Data

Frame data begins immediately after the frame table. Each frame is z-lib compressed separately. Decompressed, the frame contains a 1-bit pixel map where `0` is black and `1` is white.

### P-frames

Frame type `2` is for P-frames (frames that are based on previous frames), and these only store the pixels that have changed since the previous frame. The full image can be resolved by looping through each pixel in the frame and doing a logical XOR against the same pixel from the previous resolved frame.

For example in C this would be something like:

```c
for (int i = 0; i < sizeof(frame); i++)
{
  frame[i] ^= prevFrame[i];
}
```

### Combined I-frame and P-frame

Frame type 3 contains both I-frame and P-frame data for the same frame. This is so you can step backwards from an I-frame without having to jump to the previous I-frame then apply P-frames all the way forward. 

The frame data for this frame will start with an `uint16` giving the length of the I-frame data, followed by the I-frame data, and then the P-frame data.


================================================
FILE: formats/pdz.md
================================================
A file with the `.pdz` extension represents a file container that has been compiled by `pdc`. They mostly contain compiled Lua bytecode, but they can sometimes include other assets such as images or fonts. This format uses little endian byte order.

## Header

| Offset | Type     | Detail |
|:-------|:---------|:-------|
| `0`    | `char[12]` | Ident `Playdate PDZ` |
| `12`   | `uint32`   | [Flags](#flags)  |

### Flags

| Bitmask             | Detail                                      |
|:--------------------|:--------------------------------------------|
| `flag & 0x40000000` | If `> 0`, the data in this file is encrypted |

File encryption is (at the time of writing) only used by Catalog games' `main.pdz` file as a form of DRM. The encryption method isn't known.

## File Entries

Following the header is a list of file entries. Each entry has a header.

| Type    | Detail |
|:--------|:-------|
| `uint8`  | [Entry Flags](#entry-flags) |
| `uint24` | [Entry Data](#entry-data) length |
| `string` | Filename as null-terminated C string |
| `-` | Optional null-padding if needed to align to the next multiple of 4 bytes |

If the [Entry Type](#entry-type) flag is `5` (for a `.pda` audio file), some additional values are included:

| Type    | Detail |
|:--------|:-------|
| `uint24` | Audio sample rate in Hz |
| `uint8`  | [Audio Data Format](/format/pda.md#audio-data-format) |

### Entry Flags

| Flag | Detail |
|:-------|:-------|
| `flags & 0x80` | If `> 0`, file entry data is compressed |
| `flags & 0x7F` | [Entry Type](#entry-type) |

### Entry Type

| Flag | Detail |
|:-------|:-------|
| `0` | Unknown/unused |
| `1` | Compiled Lua bytecode ([`.luac`](/formats/luac.md)) |
| `2` | Static image ([`.pdi`](/formats/pdi.md)) |
| `3` | Animated image ([`.pdt`](/formats/pdt.md)) |
| `4` | Video ([`.pdv`](/formats/pdv.md)) |
| `5` | Audio ([`.pda`](/formats/pda.md)) |
| `6` | Text strings ([`.pds`](/formats/pds.md)) |
| `7` | Font ([`.pft`](/formats/pft.md)) |

## Entry Data

The data for a given file entry is immediately after the entry's file header. If the file's compression flag is set, this will begin with a `uint32` giving the decompressed size of the data, followed by zlib-compressed data.

All of the asset entries (`.pdi`, `.pdt`, `.pdv`, `.pda`, `.pds`, `.pft`), will be missing the first 16 bytes of the header, since for most of these formats this just contains a 12-byte format ident string and some compression flags. This is why `.pda` entries have additional header fields for the sample rate and audio format.

================================================
FILE: formats/pft.md
================================================
A file with the `.pft` extension represents a 1-bit bitmap font that has been compiled by `pdc`. This format uses little endian byte order.

## Header

| Offset | Type      | Detail               |
|:-------|:----------|:---------------------|
| `0`    | `char[12]` | Ident `Playdate FNT` |
| `12`   | `uint32`   | [Flags](#flags) |

### Flags

| Bitmask             | Detail                                      |
|:--------------------|:--------------------------------------------|
| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |
| `flags & 0x00000001` | If `> 0`, the font contains characters above U+1FFFF |

## Font Header

If the compression flag is set, there's an extra font header after the file header. Everything after this is zlib-compressed. 

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint32`  | Size of font data section when decompressed |
| `4`    | `uint32`  | Maximum glyph width (in pixels) |
| `8`    | `uint32`  | Maximum glyph height (in pixels) |

## Page List

### Page List Header

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint8`  | Glyph width (in pixels) |
| `1`    | `uint8`  | Glyph height (in pixels) |
| `2`    | `uint16`  | Tracking (in pixels) |
| `4`    | `64 bytes` | [Page Usage Flags](#page-usage-flags) |

Font glyphs are grouped into pages based on their unicode codepoints. Each page covers a span of 256 glyphs. Pages are only stored if they have glyphs present in the font.

The page index for a given glyph codepoint will be `codepoint >> 8`.

Following this header is a list of `uint32` offsets for all of the pages present in the file, with the pages following immediately after.

### Page Usage Flags

This contains bitflags for each page, starting at the lowest significant bit in the first byte. If a page's corresponding usage flag is `1`, then it is present in the file.

## Page

### Page Header

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint24`  | Reserved? Seen as `0` |
| `3`    | `uint8`  | Number of glyphs |
| `4`    | `32 bytes`  | [Glyph Usage Flags](#glyph-usage-flags) |

After the page header is a series of [Glyphs](#glyph) for the page.

### Glyph Usage Flags

This contains bitflags for each glyph, starting at the lowest significant bit in the first byte. If a glyph's corresponding usage flag is `1`, then it is present in the page.

## Glyph

Each glyph is comprised of:
 - a header
 - a short kerning table
 - (if necessary) padding bytes to align to the next multiple of 4
 - a long kerning table
 - pixel data

### Glyph Header

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint8`  | Glyph advance / width (in pixels) |
| `1`    | `uint8`  | Number of [Short Kerning Table Entries](#short-kerning-table-entries) |
| `2`    | `uint16`  | Number of [Long Kerning Table Entries](#long-kerning-table-entries) |

### Short Kerning Table Entries

I think this is for codepoints within the same page?

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint8`  | Other glyph codepoint |
| `1`    | `int8`  | Kerning (in pixels) |

### Long Kerning Table Entries

This supports any unicode codepoint within the whole font

| Offset | Type     | Detail |
|:-------|:---------|:--------------------------------|
| `0`    | `uint24`  | Other glyph codepoint |
| `3`    | `int8`  | Kerning (in pixels) |

### Glyph pixels

Stored as an [Image Cell](/formats/pdi.md#image-cell).

================================================
FILE: readme.md
================================================
Unofficial Playdate reverse-engineering notes/tools - covers file formats, server API and USB serial commands

> ⚠️ This documentation is unofficial and is not affiliated with Panic. All of the content herein was gleaned from reverse-engineering Playdate tools and game files, and as such there may be mistakes or missing information. 

## Documentation

- **File Formats**
  - **Playdate game formats**
    - [**pdex.bin**](formats/pdex.md) - Executable code
    - [**.luac**](formats/luac.md) - Lua bytecode
    - [**.pdz**](formats/pdz.md) - File container
    - [**.pda**](formats/pda.md) - Audio file
    - [**.pdi**](formats/pdi.md) - Image file
    - [**.pdt**](formats/pdt.md) - Imagetable file
    - [**.pdv**](formats/pdv.md) - Video file
    - [**.pds**](formats/pds.md) - Strings file
    - [**.pft**](formats/pft.md) - Font file
  - **Other formats**
    - [**.fnt**](formats/fnt.md) - Font source file
    - **.strings** - Strings source file (TODO)
- **Server**
  - [**Playdate API**](server/api.md) - Main Playdate server API
- **Misc**
  - [**USB**](usb/usb.md) - USB serial interface
  - [**Streaming**](usb/stream.md) - Video/audio streaming protocol (via USB serial), used by Playdate Mirror

## Tools

- [**`pdz.py`**](tools/pdz.py) - Unpacks all files from a `.pdz` file container.
- [**`pdex2elf.py`**](tools/pdex2elf.py) - Converts a `pdex.bin` to an ELF file that can be analyzed in tools such as readelf, objdump or Ghidra, or compiled back to the same original `pdex.bin` by `pdc`.
- [**`pdi2png.py`**](tools/pdi2png.py) - Converts a `.pdi` image file to a `.png` image.
- [**`usbeval.py`**](tools/usbeval.py) - Uses the Playdate's USB `eval` command to evaluate a Lua script over USB. Has access to the Lua runtime of the currently loaded game, except for system apps.

## Related Projects and Resources

- [**pd-usb**](https://github.com/jaames/pd-usb) - JavaScript library for interacting with the Playdate's serial API from a WebUSB-compatible web browser.
- [**unluac**](https://github.com/scratchminer/unluac) - Fork of the unluac Lua decompiler, modified to support Playdate-flavoured Lua.
- [**lua54**](https://github.com/scratchminer/lua54) - Fork of Lua that aims to match the custom tweaks that Panic added for Playdate-flavoured Lua.

## Special Thanks

 - [Zhuowei](https://github.com/zhuowei) for this [script for unpacking Playdate .pdx executables](https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3), which was the base for `pdz.py`
 - [Scratchminer](https://github.com/scratchminer) for their further reverse-engineering work on the Playdate's [file formats](https://github.com/scratchminer/pd-emu), streaming protocol and [Lua implementation](https://github.com/scratchminer/lua54).
 - [Simon](https://github.com/simontime) for helping with some ADPCM audio data reverse engineering
 - The folks at [Panic](https://panic.com/) for making such a wonderful and fascinating handheld!

 ----

 2022-2023 James Daniel

 Playdate is © [Panic Inc.](https://panic.com/) - this project isn't affiliated with or endorsed by them in any way.


================================================
FILE: server/api.md
================================================
This is the API that is used by the Playdate console for things like fetching game updates, scoreboards, player details, etc. It is available under `https://play.date/api/v2`. All API endpoints require [auth headers](#auth-headers).

This list of endpoints was obtained by decompiling the Playdate Simulator app. Some haven't actually been seen in use, so they are only partially documented and there may be mistakes.

## Endpoints

| Method | Path |
|:-|:-|
| `POST` | [`/auth_echo/`](#post-auth_echo) |
| `GET`  | [`/player/`](#get-player) |
| `GET`  | [`/player/:playerId/`](#get-playerplayerid) |
| `POST` | `/player/avatar/` |
| `GET`  | [`/games/scheduled/`](#get-gamesscheduled) |
| `GET`  | [`/games/user/`](#get-gamesuser) |
| `GET`  | `/games/testing/` |
| `GET`  | [`/games/system/`](#get-gamessystem) |
| `GET`  | [`/games/purchased/`](#get-gamespurchased) |
| `GET`  | [`/games/catalog`](#get-gamescatalog) |
| `GET`  | [`/games/catalog/:idx`](#get-gamescatalogidx) |
| `POST`  | [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |
| `POST`  | [`/games/:bundleId/purchase/confirm`](#post-gamesbundleidpurchaseconfirm) |
| `GET`  | `/games/:bundleId/latest_build/` |
| `GET`  | `/games/:bundleId/boards/` |
| `GET`  | `/games/:bundleId/boards/:boardId/` |
| `POST` | `/games/:bundleId/boards/:boardId/` |
| `GET`  | `/device/settings/` |

### POST /auth_echo

Seems to just return whatever JSON body is sent to it.

### GET /player

Returns the player profile for the user that owns the current access token.

### GET /player/:playerId

Same as `/player`, but gets the player profile for another user, given their [Player ID](#player-id).

### GET /games/scheduled

Returns an array of [Schedule](#Schedule) entries for any seasons that you have access to.

### GET /games/user

Returns an array of [Game](#Game) entries for games that you have [sideloaded](https://help.play.date/games/sideloading/).

### GET /games/system

Returns an array of [Game](#Game) entries for additional system applications, such as [Catalog](https://play.date/games/catalog/).

### GET /games/purchased

Returns an array of [Game](#Game) entries for games that you have purchased through [Catalog](https://play.date/games/catalog/).

### GET /games/catalog

Returns an array of [Catalog Game](#CatalogGame) entries for games that are available through [Catalog](https://play.date/games/catalog/).

### GET /games/catalog/:idx

Returns [Catalog Game](#CatalogGame) entry for a specific [Catalog](https://play.date/games/catalog/) game.

### POST /games/:bundleId/purchase/

Initiates the purchase flow for a game. Returns instructions for confirming the purchase on another device.

### POST /games/:bundleId/purchase/confirm

Completes the purchase flor for a game.

### GET /device/register/:serialNumber

If the device hasn't already been registered, returns a JSON containing its serial number and pin.

This endpoint requires an extra header:

| Header | Value |
|:-|:-|
| `Idempotency-Key` | Random 16-character string. Allowed chars are `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`. |

### GET /device/register/:serialNumber/complete

Returns a JSON with the device's registered status, access token, and serial number. Access token will only be available on the first request to this endpoint after registering the device.

## Schemas

### Schedule

| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Schedule name (Season One is `Season-001`) |
| `start_date` | string | Schedule start datetime, formatted as `E MMM d HH:mm:ss yyyy zzz` (e.g. `Mon Apr 18 00:00:00 2022 PDT`) |
| `start_date_timestamp` | number | Schedule start as a UNIX timestamp |
| `next_release_timestamp` | number | Time of next scheduled release as a UNIX timestamp, can be `null` |
| `ended` | boolean | |
| `games` | array | Array of available [Game](#Game) entries |

### Game 

| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Game name, will be displayed to the user |
| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |
| `short_description` | string | Few games currently have this (only seen on Flipper Lifter and Boogie Loops so far), often `null` |
| `studio` | string | Game's publisher/developer |
| `has_newer_build` | boolean | |
| `decryption_key` | string | Only present for purchased games, otherwise `null`. Unclear what this is actually used for - at the time of writing purchased games do not appear to be encrypted and the key changes on each request. Seems to be base64-encoded. |
| `latest_build` | [Build](#Build) | |

### Build

| Key | Type | Detail |
|:----|:------|:------|
| `url` | string | Web URL for the build's .zip file |
| `is_beta` | boolean | |
| `version` | string | Human-friendly version string, taken from the game's pdxinfo file |
| `build_number` | number | Incremental build number from the game's pdxinfo |
| `filesize` | number | .zip file size, in bytes |
| `upzipped_filesize` | number | Size of the .zip contents after decompression, in bytes |

### Catalog Game

| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Game name, will be displayed to the user |
| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |
| `studio` | string | Game's publisher/developer |
| `description` | string | Game's description |
| `detail_url` | string | Path for this game's [`/games/catalog/:idx`](#get-gamescatalogidx) endpoint |
| `price` | number | Price in USD |
| `header_image` | string | Path to .pdi image file |
| `list_image_size` | string | `"small"` |
| `list_image` | string | Path to .pdi image file |
| `animation_frame_duration` | unknown | Seen as `null` |
| `animation_frame_timing` | unknown | Seen as `null` |
| `accessibility` | string | Game accessibility information |
| `rating` | string | Game age rating |
| `screenshots` | [Catalog Screenshot](#CatalogScreenshot)[] | Game screenshots |
| `build_size` | string | Download size, e.g. `"22.8 MB"` |
| `published_date` | string | |
| `updated_date` | string | |
| `authorized` | boolean | |
| `purchasable` | boolean | |
| `short_description` | string | |
| `web_url` | string | URL for this game on the Catalog web storefront |
| `purchase_url` | string | Path for this game's [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |

### Catalog Screenshot

| Key | Type | Detail |
|:----|:------|:------|
| `url` | string | Path to .pdi image file |
| `frame_timing` | number[] | |

## Auth Headers

All routes require a basic authorization token sent via a HTTP header. 

| Header | Value |
|:-|:-|
| `Authorization` | `Token ` followed by your authorization token |

### Simulator Tokens

If you have a developer account on [play.date](//play.date), you can generate a simulator-only access token by going to `https://play.date/players/account/` and clicking 'register simulator'. It looks as though you're allowed to register up to 5 simulators at one time. Note that this will be relatively limited, as the simulator cannot.

## Player ID

Player IDs seem to use the `DCE 1.1, ISO/IEC 11578:1996` variant of UUID v4, with dashes separating each section, e.g `XXXXXXXX-XXXX-4XXX-8XXX-XXXXXXXXXXXX`.


================================================
FILE: tools/pdex2elf.py
================================================
import argparse
import hashlib
import zlib

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        prog='pdex2elf.py',
        description='Converts a Playdate pdex.bin file to an ELF file.'
    )
    parser.add_argument('pdex', help='the path to the input pdex.bin')
    parser.add_argument('elf', help='the path to the output ELF file')
    args = parser.parse_args()

    with open(args.pdex, 'rb') as pdex:
        pdex_magic = pdex.read(12)
        if pdex_magic in [b'Playdate PDX', b'Playdate BIN']:
            pdex_flags = int.from_bytes(pdex.read(4), byteorder='little')
            if pdex_flags & 0x40000000:
                raise ValueError('the specified pdex.bin is encrypted')
            pdex_version = '2.0'
            pdex_checksum = pdex.read(16)
            pdex_filesz = int.from_bytes(pdex.read(4), byteorder='little')
            pdex_memsz = int.from_bytes(pdex.read(4), byteorder='little')
            pdex_entry = int.from_bytes(pdex.read(4), byteorder='little')
            pdex_relnum = int.from_bytes(pdex.read(4), byteorder='little')
            pdex_data = zlib.decompress(pdex.read())
        else:
            pdex_entry = int.from_bytes(pdex_magic[:4], byteorder='little') - 0x6000000c
            pdex_filesz = int.from_bytes(pdex_magic[4:8], byteorder='little') - 0x6000000c
            pdex_memsz = int.from_bytes(pdex_magic[8:], byteorder='little') - 0x6000000c
            if pdex_entry < 0 or pdex_filesz < 0 or pdex_memsz < 0:
                raise ValueError('the specified file is not a pdex.bin')
            pdex_version = '1.0'
            pdex_magic = None
            pdex_flags = 0
            pdex_relnum = 0
            pdex_checksum = None
            pdex_data = pdex.read()

    print('pdex.bin info:')
    print('  Version:           {}'.format(pdex_version))
    if pdex_magic is not None:
        print('  File signature:    {}'.format(pdex_magic.decode()))
    print('  Flags:             {}'.format(pdex_flags))
    if pdex_checksum is not None:
        print('  Declared checksum: {}'.format(pdex_checksum.hex()))
        print('  Computed checksum: {}'.format(hashlib.md5(pdex_data[:pdex_filesz]).hexdigest()))
    print('  Entry point:       {}'.format(pdex_entry))
    print('  File size:         {}'.format(pdex_filesz))
    print('  Memory size:       {}'.format(pdex_memsz))
    print('  Relocations:       {}'.format(pdex_relnum))

    with open(args.elf, 'wb') as elf:
        text_index = 1
        text_addr = 0
        text_offset = 0x10000
        text_size = pdex_filesz

        bss_index = 2
        bss_addr = (pdex_filesz + 3) & ~3
        bss_offset = text_offset + bss_addr
        bss_size = pdex_memsz - pdex_filesz

        rel_text_index = 3
        rel_text_offset = bss_offset
        rel_text_size = pdex_relnum * 8

        symtab_index = 4
        symtab_offset = (rel_text_offset + rel_text_size + 3) & ~3
        symtab_size = 2 * 16

        strtab_index = 5
        strtab_data = b'\0'
        strtab_offset = symtab_offset + symtab_size
        strtab_size = len(strtab_data)

        shstrtab_index = 6
        shstrtab_data = b'\0.text\0.bss\0.rel.text\0.symtab\0.strtab\0.shstrtab\0'
        shstrtab_offset = strtab_offset + strtab_size
        shstrtab_size = len(shstrtab_data)

        sh_offset = (shstrtab_offset + shstrtab_size + 3) & ~3

        # ==== ELF header ====

        # e_ident[EI_MAG0..EI_MAG3]
        elf.write(b'\x7fELF')
        # e_ident[EI_CLASS]
        elf.write(b'\x01') # ELFCLASS32
        # e_ident[EI_DATA]
        elf.write(b'\x01') # ELFDATA2LSB
        # e_ident[EI_VERSION]
        elf.write(b'\x01') # EV_CURRENT
        # e_ident[EI_OSABI]
        elf.write(b'\x00') # ELFOSABI_SYSV
        # e_ident[EI_ABIVERSION]
        elf.write(b'\x00')
        # e_ident[EI_PAD..(EI_NIDENT - 1)]
        elf.write(b'\x00\x00\x00\x00\x00\x00\x00')
        # e_type
        elf.write(b'\x02\x00') # ET_EXEC
        # e_machine
        elf.write(b'\x28\x00') # EM_ARM
        # e_version
        elf.write(b'\x01\x00\x00\x00') # EV_CURRENT
        # e_entry
        elf.write(pdex_entry.to_bytes(4, byteorder='little'))
        # e_phoff
        elf.write(b'\x34\x00\x00\x00')
        # e_shoff
        elf.write(sh_offset.to_bytes(4, byteorder='little'))
        # e_flags
        elf.write(b'\x00\x04\x00\x05') # EF_ARM_EABI_VER5 | EF_ARM_ABI_FLOAT_HARD
        # e_ehsize
        elf.write(b'\x34\x00')
        # e_phentsize
        elf.write(b'\x20\x00')
        # e_phnum
        elf.write(b'\x01\x00')
        # e_shentsize
        elf.write(b'\x28\x00')
        # e_shnum
        elf.write(b'\x07\x00')
        # e_shstrndx
        elf.write(shstrtab_index.to_bytes(2, byteorder='little'))

        # ==== Program header ====

        # p_type
        elf.write(b'\x01\x00\x00\x00') # PT_LOAD
        # p_offset
        elf.write(text_offset.to_bytes(4, byteorder='little'))
        # p_vaddr
        elf.write(b'\x00\x00\x00\x00')
        # p_paddr
        elf.write(b'\x00\x00\x00\x00')
        # p_filesz
        elf.write(pdex_filesz.to_bytes(4, byteorder='little'))
        # p_memsz
        elf.write(pdex_memsz.to_bytes(4, byteorder='little'))
        # p_flags
        elf.write(b'\x07\x00\x00\x00') # PF_X | PF_W | PF_R
        # p_align
        elf.write(text_offset.to_bytes(4, byteorder='little'))

        # ==== .text section ====

        elf.write(b'\x00' * (text_offset - elf.tell()))

        elf.write(pdex_data[:pdex_filesz])

        # ==== .rel.text section ====

        elf.write(b'\x00' * (rel_text_offset - elf.tell()))

        for i in range(pdex_filesz, pdex_filesz + 4 * pdex_relnum, 4):
            # r_offset
            elf.write(pdex_data[i:(i + 4)])
            # r_info
            elf.write(b'\x02')
            elf.write(text_index.to_bytes(2, byteorder='little'))
            elf.write(b'\x00')

        # ==== .symtab section ====

        elf.write(b'\x00' * (symtab_offset - elf.tell()))

        # NULL
        # st_name
        elf.write(b'\x00\x00\x00\x00')
        # st_value
        elf.write(b'\x00\x00\x00\x00')
        # st_size
        elf.write(b'\x00\x00\x00\x00')
        # st_info
        elf.write(b'\x00') # STB_LOCAL, STT_NOTYPE
        # st_other
        elf.write(b'\x00')
        # st_shndx
        elf.write(b'\x00\x00')

        # .text
        # st_name
        elf.write(text_addr.to_bytes(4, byteorder='little'))
        # st_value
        elf.write(b'\x00\x00\x00\x00')
        # st_size
        elf.write(b'\x00\x00\x00\x00')
        # st_info
        elf.write(b'\x03') # STB_LOCAL, STT_SECTION
        # st_other
        elf.write(b'\x00')
        # st_shndx
        elf.write(text_index.to_bytes(2, byteorder='little'))

        # ==== .strtab section ====

        elf.write(strtab_data)

        # ==== .shstrtab section ====

        elf.write(shstrtab_data)

        # ==== Section headers ====

        elf.write(b'\x00' * (sh_offset - elf.tell()))

        # NULL
        # sh_name
        elf.write(b'\x00\x00\x00\x00')
        # sh_type
        elf.write(b'\x00\x00\x00\x00') # SHT_NULL
        # sh_flags
        elf.write(b'\x00\x00\x00\x00')
        # sh_addr
        elf.write(b'\x00\x00\x00\x00')
        # sh_offset
        elf.write(b'\x00\x00\x00\x00')
        # sh_size
        elf.write(b'\x00\x00\x00\x00')
        # sh_link
        elf.write(b'\x00\x00\x00\x00')
        # sh_info
        elf.write(b'\x00\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x00\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x00\x00\x00\x00')

        # .text
        # sh_name
        elf.write((shstrtab_data.index(b'\0.text\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x01\x00\x00\x00') # SHT_PROGBITS
        # sh_flags
        elf.write(b'\x37\x00\x00\x00') # SHF_WRITE | SHF_ALLOC | SHF_EXECINSTR | SHF_MERGE | SHF_STRINGS
        # sh_addr
        elf.write(text_addr.to_bytes(4, byteorder='little'))
        # sh_offset
        elf.write(text_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(text_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(b'\x00\x00\x00\x00')
        # sh_info
        elf.write(b'\x00\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x08\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x00\x00\x00\x00')

        # .bss
        # sh_name
        elf.write((shstrtab_data.index(b'\0.bss\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x08\x00\x00\x00') # SHT_NOBITS
        # sh_flags
        elf.write(b'\x03\x00\x00\x00') # SHF_WRITE | SHF_ALLOC
        # sh_addr
        elf.write(bss_addr.to_bytes(4, byteorder='little'))
        # sh_offset
        elf.write(bss_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(bss_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(b'\x00\x00\x00\x00')
        # sh_info
        elf.write(b'\x00\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x04\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x00\x00\x00\x00')

        # .rel.text
        # sh_name
        elf.write((shstrtab_data.index(b'\0.rel.text\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x09\x00\x00\x00') # SHT_REL
        # sh_flags
        elf.write(b'\x40\x00\x00\x00') # SHF_INFO_LINK
        # sh_addr
        elf.write(b'\x00\x00\x00\x00')
        # sh_offset
        elf.write(rel_text_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(rel_text_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(symtab_index.to_bytes(4, byteorder='little'))
        # sh_info
        elf.write(text_index.to_bytes(4, byteorder='little'))
        # sh_addralign
        elf.write(b'\x04\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x08\x00\x00\x00')

        # .symtab
        # sh_name
        elf.write((shstrtab_data.index(b'\0.symtab\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x02\x00\x00\x00') # SHT_SYMTAB
        # sh_flags
        elf.write(b'\x00\x00\x00\x00')
        # sh_addr
        elf.write(b'\x00\x00\x00\x00')
        # sh_offset
        elf.write(symtab_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(symtab_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(strtab_index.to_bytes(4, byteorder='little'))
        # sh_info
        elf.write(b'\x02\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x04\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x10\x00\x00\x00')

        # .strtab
        # sh_name
        elf.write((shstrtab_data.index(b'\0.strtab\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x03\x00\x00\x00') # SHT_STRTAB
        # sh_flags
        elf.write(b'\x00\x00\x00\x00')
        # sh_addr
        elf.write(b'\x00\x00\x00\x00')
        # sh_offset
        elf.write(strtab_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(strtab_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(b'\x00\x00\x00\x00')
        # sh_info
        elf.write(b'\x00\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x01\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x00\x00\x00\x00')

        # .shstrtab
        # sh_name
        elf.write((shstrtab_data.index(b'\0.shstrtab\0') + 1).to_bytes(4, byteorder='little'))
        # sh_type
        elf.write(b'\x03\x00\x00\x00') # SHT_STRTAB
        # sh_flags
        elf.write(b'\x00\x00\x00\x00')
        # sh_addr
        elf.write(b'\x00\x00\x00\x00')
        # sh_offset
        elf.write(shstrtab_offset.to_bytes(4, byteorder='little'))
        # sh_size
        elf.write(shstrtab_size.to_bytes(4, byteorder='little'))
        # sh_link
        elf.write(b'\x00\x00\x00\x00')
        # sh_info
        elf.write(b'\x00\x00\x00\x00')
        # sh_addralign
        elf.write(b'\x01\x00\x00\x00')
        # sh_entsize
        elf.write(b'\x00\x00\x00\x00')

    print('')
    print("ELF file successfully written to '{}'".format(args.elf))


================================================
FILE: tools/pdi2png.py
================================================
#!/usr/bin/env python3
# PDI to PNG converter
# PDI docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdi.md

from struct import unpack, pack
from zlib import compress, crc32, decompress
from argparse import ArgumentParser

PDI_IDENT = b'Playdate IMG'
PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n'

def png_chunk(chunk_type, data):
  chunk = chunk_type + data
  return pack('>I', len(data)) + chunk + pack('>I', crc32(chunk) & 0xffffffff)

def write_png(path, width, height, rows, has_alpha):
  # color type: 0 = grayscale, 4 = grayscale+alpha
  color_type = 4 if has_alpha else 0
  ihdr = pack('>IIBBBBB', width, height, 8, color_type, 0, 0, 0)

  # build raw image data: filter byte (0=none) + row pixels
  raw = bytearray()
  for row in rows:
    raw.append(0)  # filter: none
    raw.extend(row)

  with open(path, 'wb') as f:
    f.write(PNG_SIGNATURE)
    f.write(png_chunk(b'IHDR', ihdr))
    f.write(png_chunk(b'IDAT', compress(bytes(raw))))
    f.write(png_chunk(b'IEND', b''))

def read_cell(data, offset):
  clip_width, clip_height, stride, clip_left, clip_right, clip_top, clip_bottom, flags = \
    unpack('<8H', data[offset:offset + 16])
  offset += 16

  has_alpha = (flags & 0x3) > 0

  # read color bitmap (1-bit, 0=black 1=white)
  color_size = stride * clip_height
  color_data = data[offset:offset + color_size]
  offset += color_size

  # read alpha bitmap if present (1-bit, 0=transparent 1=opaque)
  alpha_data = None
  if has_alpha:
    alpha_data = data[offset:offset + color_size]
    offset += color_size

  # reconstruct full image dimensions
  full_width = clip_left + clip_width + clip_right
  full_height = clip_top + clip_height + clip_bottom

  # build pixel rows
  rows = []
  for y in range(full_height):
    if has_alpha:
      row = bytearray(full_width * 2)  # grayscale + alpha per pixel
    else:
      row = bytearray(b'\xff' * full_width)  # default white

    cy = y - clip_top
    if 0 <= cy < clip_height:
      row_offset = cy * stride
      for x in range(clip_width):
        byte_index = row_offset + (x // 8)
        bit_index = 7 - (x % 8)
        color_bit = (color_data[byte_index] >> bit_index) & 1
        color = 255 if color_bit else 0
        px = clip_left + x

        if has_alpha:
          alpha_bit = (alpha_data[byte_index] >> bit_index) & 1
          alpha = 255 if alpha_bit else 0
          row[px * 2] = color
          row[px * 2 + 1] = alpha
        else:
          row[px] = color

    rows.append(row)

  return full_width, full_height, rows, has_alpha

def convert_pdi(input_path, output_path):
  with open(input_path, 'rb') as f:
    data = f.read()

  # verify ident
  ident = data[0:12]
  if ident != PDI_IDENT:
    raise ValueError(f'Not a valid PDI file (got ident {ident!r})')

  flags = unpack('<I', data[12:16])[0]
  is_compressed = (flags & 0x80000000) > 0

  offset = 16

  if is_compressed:
    decompressed_size, width, height, reserved = unpack('<4I', data[offset:offset + 16])
    offset += 16
    image_data = decompress(data[offset:])
  else:
    image_data = data[offset:]

  full_width, full_height, rows, has_alpha = read_cell(image_data, 0)
  write_png(output_path, full_width, full_height, rows, has_alpha)
  print(f'Saved {output_path} ({full_width}x{full_height})')

if __name__ == '__main__':
  parser = ArgumentParser(description='Convert Playdate .pdi images to .png')
  parser.add_argument('input', help='Input .pdi file path')
  parser.add_argument('-o', '--output', help='Output .png file path (default: input with .png extension)')
  args = parser.parse_args()

  output = args.output
  if not output:
    if args.input.lower().endswith('.pdi'):
      output = args.input[:-4] + '.png'
    else:
      output = args.input + '.png'

  convert_pdi(args.input, output)


================================================
FILE: tools/pdz.py
================================================
# based on https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3
# PDZ docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdz.md

from sys import exit
from os import path, makedirs
from struct import pack, unpack
from zlib import decompress
from argparse import ArgumentParser

PDZ_IDENT = b'Playdate PDZ'
FILE_TYPES = {
  1: 'luac',
  2: 'pdi',
  3: 'pdt',
  4: 'pdv',
  5: 'pda',
  6: 'pds',
  7: 'pft',
}
FILE_IDENTS = {
  'pdi': b'Playdate IMG',
  'pdt': b'Playdate IMT',
  'pdv': b'Playdate VID',
  'pda': b'Playdate AUD',
  'pds': b'Playdate STR',
  'pft': b'Playdate FNT'
}

class PlaydatePdz:
  @classmethod
  def open(cls, path):
    with open(path, "rb") as buffer:
      return cls(buffer)

  def __init__(self, buffer):
    self.buffer = buffer
    self.entries = {}
    self.num_entries = 0
    self.read_header()
    self.read_entries()

  def read_header(self):
    self.buffer.seek(0)
    magic = self.buffer.read(16)
    magic = magic[:magic.index(b'\0')] # trim null bytes
    assert magic == PDZ_IDENT, 'Invalid PDZ file ident'
    self.buffer.seek(12)
    flags = unpack('<I', self.buffer.read(4))[0]
    is_encrypted = (flags & 0x40000000) > 0
    assert not is_encrypted, 'PDZ file is encrypted'

  def read_string(self):
    res = b''
    while True:
      char = self.buffer.read(1)
      if char == b'\0': break
      res += char
    return res.decode()

  def read_entries(self):
    self.buffer.seek(0, 2)
    ptr = 0x10
    pdz_len = self.buffer.tell()
    self.buffer.seek(ptr)
    while ptr < pdz_len:
      head = unpack('<I', self.buffer.read(4))[0]
      flags = head & 0xFF
      entry_len = (head >> 8) & 0xFFFFFF
      # doesn't seem to be any other flags
      is_compressed = (flags >> 7) & 0x1
      file_type = FILE_TYPES[flags & 0xF]
      # file name is a null terminated string
      file_name = self.read_string()
      # align offset to next nearest multiple of 4
      self.buffer.seek((self.buffer.tell() + 3) & ~3)
      # .pda files have two more values after filename before data
      if file_type == 'pda':
        entry_len -= 4
        audio_info = unpack('<I', self.buffer.read(4))[0]
        audio_rate = audio_info & 0xFFFFFF
        audio_format = (audio_info >> 24) & 0xFF 
      # if compression flag is set, there's another uint32 with the decompressed size
      if is_compressed:
        decompressed_size = unpack('<I', self.buffer.read(4))[0]
        entry_len -= 4
      else:
        decompressed_size = entry_len

      data = self.buffer.read(entry_len)
      ptr = self.buffer.tell()
      
      self.num_entries += 1
      self.entries[file_name] = {
        'name': file_name,
        'type': file_type,
        'data': data,
        'size': entry_len,
        'compressed': is_compressed,
        'decompressed_size': decompressed_size
      }
      if file_type == 'pda':
        self.entries[file_name].update({
          'audio_rate': audio_rate, 
          'audio_format': audio_format})
  
  def get_entry_data(self, name):
    assert name in self.entries
    entry = self.entries[name]
    if entry['compressed']:
      return decompress(entry['data'])
    return entry['data']
  
  def construct_entry_header(self, name):
    # this is probably incorrect, use at your own risk
    assert name in self.entries
    entry = self.entries[name]
    file_type = entry['type']
    is_compressed = entry['compressed']
    assert file_type in ['pdi','pdt','pdv','pda','pds','pft']
    ident = FILE_IDENTS[file_type]
    if file_type == 'pda':
      rate = entry['audio_rate']
      fmt = entry['audio_format']
      audio_info = (fmt << 24) + rate
      header = pack('<12sI', ident, audio_info)
    else:
      flags = 0x80000000 if is_compressed else 0x00000000
      header = pack('<12sI', ident, flags)
    return header

  def save_entry_data(self, name, outdir, gen_header):
    assert name in self.entries
    print(f'processing entry: {name}')
    entry = self.entries[name]
    file_type = entry['type']
    data = self.get_entry_data(name)
    filepath = outdir + '/' + entry['name'] + '.' + entry['type']
    if '/' in filepath:
      makedirs(path.dirname(filepath), exist_ok=True)
    with open(filepath, 'wb') as outfile:
      if gen_header and file_type in ['pdi','pdt','pdv','pda','pds','pft']:
        hdr = self.construct_entry_header(name)
        outfile.write(hdr)
      outfile.write(data)

  def save_entries(self, outdir, gen_header):
    for name in self.entries:
      self.save_entry_data(name, outdir, gen_header)

  def print_entries(self):
    for name in self.entries:
      print(f'{name}: {self.entries[name]["type"]}')

if __name__ == "__main__":
  parser = ArgumentParser(prog="pdz.py", description="Extract contents of a pdz file.")
  parser.add_argument("-o", "--outdir", default="pdz_output", help="output directory", dest="out_dir")
  parser.add_argument("-i", "--infile", help="input file", dest="in_file", required=True)
  parser.add_argument("-l", "--list-files", help="print a list of all entries in the file, ignoring all other arguments",
                      dest="list_files", required=False, action="store_true")
  parser.add_argument("-g", "--gen-headers", help="generate file headers for pd* files (experimental, default=false)" , 
                      dest="gen_headers", required=False, action="store_true")
  parser.add_argument("-f", "--extract-file", help="extract the given file(s), or all if this arg isn't provided",
                      dest="file_list", required=False, action="append")
  args = parser.parse_args()

  pdz = PlaydatePdz.open(args.in_file)
  
  if args.list_files:
    pdz.print_entries()
    exit()
  
  if args.file_list:
    for f in args.file_list:
      pdz.save_entry_data(f, args.out_dir, args.gen_headers)
  else:
    pdz.save_entries(args.out_dir, args.gen_headers)


================================================
FILE: tools/usbeval.py
================================================
if (len(argv) < 2):
  print('usbeval.py')
  print('Evaluates a Lua script on a Playdate device, via USB')
  print('Requires pdc from the Playdate SDK as well as the pyusb library')
  print('Usage:')
  print('python3 usbeval.py ./input.lua')
  exit()

import tempfile
import subprocess
import usb.core
import usb.util
from sys import argv
from pathlib import Path
from struct import unpack
from zlib import decompress
from time import sleep

# Playdate USB vendor and product IDs
PLAYDATE_VID = 0x1331;
PLAYDATE_PID = 0x5740;
IN_SIZE = 64;

def pdz_extract_entry(data, entry):
  ptr = 0x10
  while ptr < len(data):
    flags = data[ptr]
    is_compressed = (flags >> 7) & 0x1
    innerlen = data[ptr + 1] | (data[ptr + 2] << 8)
    filename = data[ptr + 4 : data.find(b"\0", ptr + 4)]
    outerheadersize = 4 + len(filename) + 1
    outerheadersize = ((ptr + outerheadersize + 3) & ~3) - ptr
    zlibdata = data[ptr + outerheadersize + 4: ptr + outerheadersize + innerlen]
    if filename.decode('utf-8') == entry:
      return decompress(zlibdata)
    ptr += outerheadersize + innerlen
  return None

def usb_connect():
  # find our playdate device
  device = usb.core.find(idVendor=PLAYDATE_VID, idProduct=PLAYDATE_PID)
  if device is None:
    raise ValueError('Device not found')

  # set the active configuration. With no arguments, the first
  # configuration will be the active one
  device.set_configuration()

  # get an endpoint instance
  cfg = device.get_active_configuration()
  intf = cfg[(1,0)]

  epOut = usb.util.find_descriptor(
      intf,
      # match the first OUT endpoint
      custom_match = \
      lambda e: \
          usb.util.endpoint_direction(e.bEndpointAddress) == \
          usb.util.ENDPOINT_OUT)

  epIn = usb.util.find_descriptor(
      intf,
      # match the first IN endpoint
      custom_match = \
      lambda e: \
          usb.util.endpoint_direction(e.bEndpointAddress) == \
          usb.util.ENDPOINT_IN)

  assert epOut is not None
  assert epIn is not None
  device.reset()
  return epOut, epIn

def usb_read_bytes(endPoint):
  res = bytearray()
  has_started = False
  while True:
    try:
      b = bytearray(epIn.read(IN_SIZE))
      res += b
      if b != b'': has_started = True
      if has_started and b == b'': break
    except usb.core.USBTimeoutError:
     break
  return res

with tempfile.NamedTemporaryFile(prefix='main', suffix='.lua') as luafile, tempfile.TemporaryDirectory(suffix='.pdx') as pdxdir:

  # copy lua file
  print('reading input file')
  with open(argv[1], 'rb') as infile:
    luafile.write(infile.read())
    luafile.seek(0)

  # compile lua with pdc
  print('compiling lua with pdc')
  subprocess.run(['pdc', luafile.name, pdxdir])
  luastem = Path(luafile.name).stem

  # extract lua bytecode from pdz
  print('extracting lua bytecode')
  with open(Path(pdxdir, luastem + '.pdz'), 'rb') as pdzfile:
    pdz = pdzfile.read()
    bytecode = pdz_extract_entry(pdz, luastem)

  # connect to playdate over usb 
  print('finding playdate connected to usb...')
  epOut, epIn = usb_connect()
  print('successfully connected to playdate!')

  # set usb echo mode to off
  print('setting usb echo to off')
  epOut.write('echo off\n')
  resp = usb_read_bytes(epIn)

  # get version info (to test things work, but also looks cool :^))
  # print('playdate version info:')
  # epOut.write('version\n')
  # resp = usb_read_bytes(epIn)
  # print(resp.decode("utf-8").strip())

  # consume printed console content until there's nothing new
  print('clearing current console data')
  epOut.write('eval\n')
  sleep(.2)
  usb_read_bytes(epIn)
  
  # send lua bytecode to the device
  print('sending payload for device to eval...')
  header = b'eval %d\n' % len(bytecode)
  payload = header + bytecode
  epOut.write(payload)
  sleep(.2) # payload seems to take a bit to execute, you may need to adjust this if you have a big payload
  resp = usb_read_bytes(epIn)
  print('===============')
  print('console output:')
  print(resp.decode("utf-8").strip())

  # keep polling for new console output
  while True:
    try:
      sleep(.1)
      resp = usb_read_bytes(epIn)
      text = resp.decode("utf-8").strip()
      if text: print(text)
    except KeyboardInterrupt:
      break

================================================
FILE: usb/stream.md
================================================
# The `stream` protocol

When `stream enable` is sent over serial, the Playdate enters streaming mode, used by Playdate Mirror.
In this mode, the device continuously reports the state of its screen, audio engine, buttons, and crank over serial, using a special protocol to do so.

(As with all Playdate formats, all numbers reported using the protocol are little-endian.)

### Entering, exiting, and maintaining streaming mode
If the Playdate has been in streaming mode for over one second without a `stream poke` command sent over USB, it will stop reporting data.
However, it won't completely exit streaming mode -- any future `stream enable` will *not* resend the entire screen afterward.

To exit streaming mode, send `stream disable` via serial.

### Stream messages
While the Playdate is streaming, it will report data in short messages.
The general format of one of these messages is:

| **Offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Message type (see below) |
| 2 | `uint16` | Payload length in bytes |

The actual payload data follows this 4-byte header.

### Audio control
To control how audio is reported, there are three options you can send during streaming mode.
(The format the samples are parsed with isn't actually changed until the format switch is acknowledged with a stream message!)

| **Command** | **Meaning** |
|:-----|:-----|
| `stream a+` | Switch to stereo signed 16-bit PCM audio |
| `stream am` | Switch to mono signed 16-bit PCM audio |
| `stream a-` | Don't send any audio (the default) |

## Message types

### `0x0001`: Input state
Reports the state of the Playdate's buttons and crank every frame.

| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Button flags (see below) |
| 2 | `uint16` | Unknown: Seems to change erratically |
| 4 | `float` | Crank angle in degrees |

#### Button flags
| **Bitmask** | **Meaning** |
|:-----|:-----|
| `flags & 0x0001` | If `> 0`, d-pad left button is pressed |
| `flags & 0x0002` | If `> 0`, d-pad right button is pressed |
| `flags & 0x0004` | If `> 0`, d-pad up button is pressed |
| `flags & 0x0008` | If `> 0`, d-pad down button is pressed |
| `flags & 0x0010` | If `> 0`, B button is pressed |
| `flags & 0x0020` | If `> 0`, A button is pressed |
| `flags & 0x0040` | If `> 0`, Menu button is pressed |

### `0x000A`: New frame (no delay)
Starts a new frame as fast as possible, with no delay from the previous frame.

### `0x000B`: End frame
Ends the current frame. In the official Mirror app, this flips the framebuffer to make it visible on-screen.

### `0x000C`: Update screen line
Signals that a single horizontal line of the display was updated.

Only the lines that have changed since the last frame are sent, unless the current frame is the first one.
In that case, the entire screen is sent as 240 line update messages.

| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint8` | Line number that was updated (starting at one and bit-reversed, so line 0 becomes `0b10000000 == 0x80`) |
| 1 | `50 bytes` | Line data, left to right, with the least significant bit coming first in each byte |
| 51 | `uint8` | Zero byte to pad the length to a multiple of 4 bytes |

### `0x000D`: New frame (with delay)
Starts a new frame a certain amount of time since the previous frame started.

| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint32` | Milliseconds since the start of the previous frame |

### `0x0014`: Audio frames (multiple)
Sends one or multiple audio frames to buffer for playback.

The data format is signed 16-bit PCM, with the left channel of each sample coming first.
(If the audio format is mono, then the right channel will be zero.)

### `0x0015`: Audio format switch acknowledge
Signals that a requested change in audio formats has taken place.

| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Audio format flags (see below) |

#### Audio format flags
| **Bitmask** | **Meaning** |
|:-----|:-----|
| `flags & 0x0001` | If `> 0`, audio is enabled |
| `flags & 0x0002` | If `> 0`, audio has two channels |

### `0x0016`: Audio frames (fill single)
Sends one audio sample to continuously play until more data is received. This usually denotes silence.

Like with type `0x0014`, the data format is signed 16-bit PCM, with the left channel of the sample coming first.


================================================
FILE: usb/usb.md
================================================
When connected to USB and unlocked, the Playdate provides a serial interface over USB. Commands are sent as ascii and must end in a newline character (`\n`). If the currently running game logs something to the console (e.g. via `print()` in Lua, or `playdate->system->logToConsole` in C) this will also be sent via serial output. Some commands (such as `button` or `stream enable`) will cause the Playdate to continually send data until another command is sent to cancel it, other commands (for example, `bitmap`) may require extra binary data to be sent after the newline character.

Some of these commands are used by Playdate Simulator for features like "preview bitmap" or "run pdx", or to enable streaming in Playdate mirror. If you want to play around with these, and you use a browser that supports WebSerial, you should check out my [pd-usb](https://github.com/jaames/pd-usb) library.

## USB details


|||
|:----------------|:------|
| *USB Vendor ID* | `0x1331` |
| *USB Product ID* | `0x5740` |
| *Baud rate* | `115200` |

## Connecting to the USB

You can use any terminal emulator to connect to the virtual serial port. For example, on macOS, you can connect using picocom by running:

`picocom -b 115200 -p n -d 8 --omap crcrlf /dev/cu.usbmodemPD<serial number>`

When you're done, disconnect and quit picocom with `CTRL-a` `CTRL-x`

## USB commands

Running `help` will return a very helpful list of available commands:

```
The following commands are available:

Telnet commands:
 help        Displays all available commands or individual help on each command

CPU Control:
 serialread  Print the device serial number
 trace       trace_<delay>. (trace 10)
 stoptrace   stoptrace
 bootdisk    reboot into recovery segment USB disk
 datadisk    reboot into data segment USB disk
 factoryreset factory reset
 formatdata  format data disk
 settime     sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2
 gettime     reads the RTC
 vbat        get battery voltage
 rawvbat     get raw battery adc value
 batpct      get battery percentage
 temp        get estimated ambient temperature
 dcache      dcache <on/off>: turn dcache on or off
 icache      icache <on/off>: turn icache on or off

Runtime control:
 echo        echo (on|off): turn console echo on or off
 buttons     Test buttons & crank
 tunebuttons tunebuttons <debounce> <holdoff>
 btn         btn <btn>: simulate a button press. +a/-a/a for down/up/both
 changecrank changecrank +-<degrees>
 dockcrank   simulates crank docking
 enablecrank Reenables crank updates
 disablecrank Disables crank updates
 accel       simulate accelerometer change
 screen      Dump framebuffer data (400x240 bits)
 bitmap      Send bitmap to screen (followed by 400x240 bits)
 controller  start or stop controller mode
 eval        execute a compiled Lua function
 run         run <path to pdx>: Run the named program
 luatrace    Get a Lua stack trace
 stats       Display runtime stats
 autolock    autolock <always|onBattery>
 version     Display build target and SDK version
 memstats    memstats
 hibernate   hibernate

Stream:
 stream      stream <enable|disable|poke>

ESP functions:
 espreset    reset the ESP chip
 espoff      turn off the ESP
 espbootlog  get the ESP startup log
 espfile     espfile <path> <address> <md5> <uncompressed size>: add the given file to the upload list. If <uncompressed size> is added then the file is assumed to be compressed.
 espflash    espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.
 espbaud     espbaud <speed> [cts]
 esp         esp <cmd>: Forward a command to the ESP firmware, read until keypress

Firmware Update:
 fwup        fwup [bundle_path]

```

Secret commands:

```
formatboot
unlock
islocked
```

Most of these commands are self-explanatory, so I will just detail some of the interesting/different ones. Please note these were run on an older Playdate Developer Preview unit, so there may be some differences with the final units that get shipped to the public.

### `version`

Example output:

```
~version:
target=DVT1
build=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649
boot_build=c4abdb37253e-1.7.0-release.127473-buildbot
SDK=1.7.0
pdxversion=10500
serial#=<REDACTED>
cc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
```

### `stats`

Example output:

```
~stats:
frame count: 194503
frame time: 0.000977
gc time: 0.016602
disp time: 18
current time: 9855691
mem alloced: 403288
mem reserved: 460448
mem total: 16645684
kernel: 0.1%
serial: 0.0%
game: 2.5%
GC: 35.2%
wifi: 0.0%
trace: 0.0%
audio: 0.2%
```

### `buttons`

Begins button-testing mode causes the device to begin continually writing the current control state to USB bulk in, at approximately 50 times per second. This can be stopped by sending a newline to the device.

Each new state will be written as a single line with the following structure:

```
buttons:XX XX XX crank:X.X docked:X
```

`button` gives three hex-formatted numbers containing the current button state. The first number indicates which buttons are currently pressed, the second indicates which buttons were pressed after the last update, and the third indicates which buttons were released after the last update. These should be treated as bitflags:

| Button | Bitmask |
|:-------|:--------|
| a      | `0x01`   |
| b      | `0x02`   |
| up     | `0x04`   |
| down   | `0x08`   |
| left   | `0x10`  |
| right  | `0x20`  |
| menu   | `0x40`  |
| lock   | `0x80`  |

`crank` gives the crank angle as a floating point number, measured in degrees, with `0` being the 12 o'clock position.

`docked` will be `0` if the crank is not docked, or `1` if docked.

### `screen`

Gets the current screen buffer as a 1-bit array of pixels. The data returned from the Playdate will begin with an 11-byte string `\r\nscreen~:\n`, followed by 12000 bytes of bitmap data where each bit of every byte represents one pixel; `0` for black, `1` for white.

### `bitmap`

Sends a 1-bit bitmap to be previewed on the Playdate screen. The command must begin with a 7-byte command string `bitmap\n`, followed by 12000 bytes of bitmap data where each bit of every byte represents one pixel; `0` for black, `1` for white.

### `run`

Launches a .pdx rom from the Playdate's data partition. The game path must begin with a forward slash, e.g `run /System/Crayons.pdx`.

### `eval`

Evaluates a compiled Lua function on the device. The command must begin with the string `eval %d\n` where `%d` is the length of the data to eval. This should then be followed by the data for a compiled Lua function. You can use the `usbeval.py` script in this repo's `tools` directory to play around with this.

*Note: `eval` doesn't work in System apps (Launcher, Settings, etc) nor games purchased from the Catalog. You must remove `hash` from the purchased game in order to allow for eval.*

This command will not work if the currently loaded game is from the System directory on the device, presumably for security reasons.

### `stream`

Used for interacting with the Playdate's [video/audio streaming protocol](/usb/stream.md), as used by Playdate Mirror.

### `esp`

Using the `esp <cmd>` command will forward an [ESP-AT](https://docs.espressif.com/projects/esp-at/en/latest/Get_Started/What_is_ESP-AT.html) command to the ESP32 firmware. Please note that these commands can potentially be very dangerous and may even damage your Playdate, so only mess with this stuff if you're stupid (me) or know what you're doing (not me). In the event that you do goof something up, you may be able to recover by holding down the Playdate's secret power/reset button, which is hidden in the cavity where the crank handle goes when it's docked. Have paperclips on standby!

After sending an ESP-AT command, you need to continue reading from the device until you receive a line that contains `OK` or `ERROR` to get the full response.

I can't profess to be very experienced here, so I didn't poke to deeply. However here's the results of some of the commands I tried:

[**`AT+GMR`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-gmr-check-version-information):

Version information:

```
AT version:2.0.0.0-dev(b6850a4 - Oct 24 2019 12:10:13)
SDK version:v3.3-beta3-170-g91f29bef17
compile time(e9c8abb):Dec 15 2021 20:08:07
```

[**`AT+CMD?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-cmd-list-all-at-commands-and-types-supported-in-current-firmware):

Querying supported commands doesn't seem to be supported.

[**`AT+UART_CUR?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-uart-cur-current-uart-configuration-not-saved-in-flash):

Current UART configuration:

```
+UART_CUR:2534653,8,1,0,3
```

[**`AT+SYSFLASH?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-sysflash-query-set-user-partitions-in-flash):

Querying user partitions in ESP flash:

```
AT+SYSFLASH?
+SYSFLASH:"ble_data",64,1,0x21000,0x3000
+SYSFLASH:"server_cert",64,2,0x24000,0x2000
+SYSFLASH:"server_key",64,3,0x26000,0x2000
+SYSFLASH:"server_ca",64,4,0x28000,0x2000
+SYSFLASH:"client_cert",64,5,0x2a000,0x2000
+SYSFLASH:"client_key",64,6,0x2c000,0x2000
+SYSFLASH:"client_ca",64,7,0x2e000,0x2000
+SYSFLASH:"factory_param",64,8,0x30000,0x1000
+SYSFLASH:"wpa2_cert",64,9,0x31000,0x2000
+SYSFLASH:"wpa2_key",64,10,0x33000,0x2000
+SYSFLASH:"wpa2_ca",64,11,0x35000,0x2000
+SYSFLASH:"mqtt_cert",64,12,0x37000,0x2000
+SYSFLASH:"mqtt_key",64,13,0x39000,0x2000
+SYSFLASH:"mqtt_ca",64,14,0x3b000,0x2000
+SYSFLASH:"fatfs",1,129,0x70000,0x90000
```

Most partitions don't seem to be readable, however you can for example dump the client CA cert by doing `AT+SYSFLASH=2,"client_ca",0,0x2000`.

[**`AT_FS`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#esp32-only-at-fs-filesystem-operations):

No file system commands seem to be supported.

### `btn`

The `btn` command can also send key presses and releases, which call the associated callbacks for the current game.

Most keys on the computer keyboard have a single-byte representation in the `btn` command. In the case of printable characters, this corresponds to their ASCII codepoint, but other keys like arrows and function keys also have a number used by the Simulator.

You can find a full set of the key codes used by the Simulator [here](https://gist.github.com/scratchminer/dcbf3410a7e72151ea68273adce9c932).

For a key press event, send `btn vK`, where `K` is the key code byte.

For a key release event, send `btn ^K`, again where `K` is the key code byte.

### `unlock`

Takes a 32 character unlock code and compares it to the device's unlock code.

If it matches, [additional commands](usb_unlocked.md) are enabled.

The unlock code is likely unique to each device and is located in different memory regions depending
on the playdate hardware revision.
- As of firmware 1.10, HW revision A contains the key at address `0x1FF0F040`.
- HW revision B contains the key at address `0x08FFF040`, located in the OTP region of the flash memory.

The following code snippet may be used to dump your key. Use the memory location that corresponds to
the HW revision you have. Even though the memory region is protected in unprivileged mode, in firmware
2.4.2, the write succeeds and then the console crashes. You may then find the key in the data drive.

```c
SDFile* file = pd->file->open("unlockkey.txt", kFileWrite);
pd->file->write(file, (const char*)0x1FF0F040, 0x20);
pd->file->close(file);
```

### `islocked`

Prints `1` if the serial console is locked, or `0` if the device successfully ran `unlock`.

## Changes

### 2.0

- Added `rawvbat` command

### 1.12.3

(these commands were observed in 1.12.3, but may have been introduced earlier)

- Added `factoryreset` command
- Added `tunebuttons` command
- Changed `autolock` to remove never option.

### 1.7.0

- Added the `hibernate` command.
- `version` output:
  ```
  ~version:
  target=DVT1
  build=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649
  boot_build=c4abdb37253e-1.7.0-release.127473-buildbot
  SDK=1.7.0
  pdxversion=10500
  serial#=<REDACTED>
  cc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
  ```

### 1.4.0

Initial version tested


================================================
FILE: usb/usb_unlocked.md
================================================
## USB commands in unlocked mode

This is the output of running `help` on a Playdate with firmware 1.10 after running `unlock`:

```
The following commands are available:

Telnet commands:
 help        Displays all available commands or individual help on each command
 loop        loop next command x times with delay y ms (eg loop 10 100 help)
 lock        Lock the firmware command interface

eMMC kvstore:
 kvget       kvget <key> [len]
 kvput       kvput <key> <len>
 kvrm        kvrm <key>
 kvwipe      kvwipe

CPU Control:
 gpio        gpio <port> <i(pu/pd)/1(pu/pd)/0(pu/pd)>. (gpio A3 1 or gpio B2 ipu)
 i2cread     i2cread <unit> <address> <register> <len>. (i2cread 0 0x47 0x38 3)
 i2cwrite    i2cwrite <unit> <address> <register> <value>. (i2cwrite 3 0x47 0x38 0xff)
 tlv493read  tlv493read <address> <len>. (tlv493read 0x47 3)
 tlv493write tlv493write <address> <val>. (tlv493write 0x47 0x00 0x37 0x48)
 serialwrite serialwrite <string>
 serialread  Print the device serial number
 pwwrite     pwwrite <string>
 pwread      pwread
 reset       reset system
 dfu         reset system
 trace       trace_<delay>. (trace 10)
 stoptrace   stoptrace
 bootdisk    reboot into recovery segment USB disk
 datadisk    reboot into data segment USB disk
 sysdisk     reboot into system segment USB disk
 factoryreset factory reset
 formatboot  format recovery disk
 formatdata  format data disk
 formatsys   format system disk
 shutdown    put device in stop mode
 cleartime   clear time from device
 settime     sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2
 gettime     reads the RTC
 rtccalib    rtccalib <on/off>: enable rtc calibration output
 vbat        get battery voltage
 batpct      get battery percentage
 temp        get estimated ambient temperature
 peek        peek <addr>: Read a 32-bit value from memory
 poke        poke <addr> <value>: Write a 32-bit value int memory
 dump        dump <start> <end>: Dump memory in range [start-end) as bytes
 dumpw       dumpw <start> <end>: Dump memory in range [start-end) as words
 stop        stop cpu
 led         led <R> <G> <B>. (led 0-255 0-255 0-255)
 leddemo     leddemo
 charge      charge <on/off>: enables/disables battery charging
 readcharger readcharger
 burn        waste cpu to run down the battery
 lsedrive    get LSE drive level
 extv        get 5V_ext voltage
 dcache      dcache <on/off>: turn dcache on or off
 icache      icache <on/off>: turn icache on or off
 rcccsr      report bootinfo.rcccsr value
 rdp         rdp <enable/disable>
 suspendusb  sets USB current limit to 500uA

eMMC Control:
 emmctest    emmctest
 emmcinfo    emmcinfo
 emmcwipe    emmcwipe
 emmcdump    emmcdump <addr> <len>

Audio Control:
 audiotest   Send test data to audio output
 audiosweep  audiosweep <startfreq> <endfreq> <length>: Send sweep signal to audio output. Frequencies are (integer) Hz, length is milliseconds
 stopaudio   Stop audio output
 startaudio  Start audio output
 audioout    audioout <speaker|headphone|both|none|bt|onboard>
 mictest     mictest <int|ext> <filename> <length>: Record microphone to given file. length is in milliseconds
 blowtest    blowtest
 micbiastest micbiastest <level:0-5> <verbose:0/1>

Encryption:
 encrypt     encrypt <file_in> <file_out>
 decrypt     decrypt <file_in> <file_out>

Runtime control:
 echo        echo (on|off): turn console echo on or off
 buttons     Test buttons & crank
 tunebuttons tunebuttons <debounce> <holdoff>
 btn         btn <btn>: simulate a button press. +a/-a/a for down/up/both
 changecrank changecrank +-<degrees>
 dockcrank   simulates crank docking
 enablecrank Reenables crank updates
 disablecrank Disables crank updates
 accel       simulate accelerometer change
 pause       Pause execution
 resume      Resume execution
 restart     Restart Lua runtime
 step        Step one frame
 fps         fps to return current frame rate, fps <frame rate> to set
 screen      Dump framebuffer data (400x240 bits)
 bitmap      Send bitmap to screen (followed by 400x240 bits)
 lcdtest     Draw a marching stripes pattern on the screen
 controller  start or stop controller mode
 eval        execute a compiled Lua function
 run         run <path to pdx>: Run the named program
 whatsrunning Returns the path of the currently running program
 luatrace    Get a Lua stack trace
 stats       Display runtime stats
 autolock    autolock <always|onBattery>
 version     Display build target and SDK version
 station     station <travel agent station name>: Configure device for running at the named station
 setvolume   setvolume <amt>:, 0-255
 getvolume   Returns the volume level 0-255
 wifi        wifi <GET|POST> <url> [file1] [file2]
 wifitest    wifitest <network> <password>
 memtest     memtest
 memstats    memstats
 woprset     woprset [server]
 woprget     woprget
 syncperiod  syncperiod <initial_s> [period_s]
 hibernate   hibernate

Filesystem Stuff:
 listfiles   listfiles path: list files at path
 getfile     getfile <path>: get file contents at path
 putfile     putfile <path> <size>: upload file to path
 mkdir       mkdir <path>
 delete      delete <path>
 rmdir       rmdir <path>: Recursively delete directory
 unzip       unzip zip_path out_path
 md5         md5 <file_path>

memio commands:
 memiomd5    memiomd5 <addr> <size>

Stream:
 stream      stream <enable|disable|poke>

ESP functions:
 espreset    reset the ESP chip
 espoff      turn off the ESP
 espbootlog  get the ESP startup log
 espfile     espfile <path> <address> <md5> <uncompressed size>: add the given file to the upload list. If <uncompressed size> is added then the file is assumed to be compressed.
 espflash    espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.
 espbaud     espbaud <speed> [cts]
 esp         esp <cmd>: Forward a command to the ESP firmware, read until keypress
 esptest     esptest <baud> <cts>
 espwipe     espwipe
 espversion  espversion
 wifiperf    wifiperf <addr> <port>
 wifistop    wifistop

Firmware Update:
 fw          fw
 fwup        fwup [bundle_path]
 recovery    recovery
 unstage     unstage

unzip test:
 unzipmem    unzipmem <addr> <len> <path>

Memfault Tests:
 hardfault   Trigger a hardfault
 assert      Trigger an assert
 mflt        mflt <status|clear|send>

```

## Changes

### 1.12.x

- Added `factoryreset`
- Added `tunebuttons`
- Added `never` option for `autolock`
Download .txt
gitextract_ijazx7lw/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── formats/
│   ├── fnt.md
│   ├── luac.md
│   ├── pda.md
│   ├── pdex.md
│   ├── pdi.md
│   ├── pds.md
│   ├── pdt.md
│   ├── pdv.md
│   ├── pdz.md
│   └── pft.md
├── readme.md
├── server/
│   └── api.md
├── tools/
│   ├── pdex2elf.py
│   ├── pdi2png.py
│   ├── pdz.py
│   └── usbeval.py
└── usb/
    ├── stream.md
    ├── usb.md
    └── usb_unlocked.md
Download .txt
SYMBOL INDEX (18 symbols across 3 files)

FILE: tools/pdi2png.py
  function png_chunk (line 12) | def png_chunk(chunk_type, data):
  function write_png (line 16) | def write_png(path, width, height, rows, has_alpha):
  function read_cell (line 33) | def read_cell(data, offset):
  function convert_pdi (line 85) | def convert_pdi(input_path, output_path):

FILE: tools/pdz.py
  class PlaydatePdz (line 29) | class PlaydatePdz:
    method open (line 31) | def open(cls, path):
    method __init__ (line 35) | def __init__(self, buffer):
    method read_header (line 42) | def read_header(self):
    method read_string (line 52) | def read_string(self):
    method read_entries (line 60) | def read_entries(self):
    method get_entry_data (line 106) | def get_entry_data(self, name):
    method construct_entry_header (line 113) | def construct_entry_header(self, name):
    method save_entry_data (line 131) | def save_entry_data(self, name, outdir, gen_header):
    method save_entries (line 146) | def save_entries(self, outdir, gen_header):
    method print_entries (line 150) | def print_entries(self):

FILE: tools/usbeval.py
  function pdz_extract_entry (line 24) | def pdz_extract_entry(data, entry):
  function usb_connect (line 39) | def usb_connect():
  function usb_read_bytes (line 74) | def usb_read_bytes(endPoint):
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 62,
    "preview": "# These are supported funding model platforms\n\ngithub: jaames\n"
  },
  {
    "path": ".gitignore",
    "chars": 28,
    "preview": "testing\ntools/*lua\n.obsidian"
  },
  {
    "path": "LICENSE",
    "chars": 7048,
    "preview": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n"
  },
  {
    "path": "formats/fnt.md",
    "chars": 3604,
    "preview": "A file with the `.fnt` extension contains font data created by [playdate caps](https://play.date/caps/). It is a line or"
  },
  {
    "path": "formats/luac.md",
    "chars": 5617,
    "preview": "Lua-based Playdate games use a tweaked version of Lua 5.4.3. You will only find compiled Lua bytecode in [`.pdz`](/forma"
  },
  {
    "path": "formats/pda.md",
    "chars": 2228,
    "preview": "A file with the `.pda` extension represents audio data that has been compiled by `pdc`. This format uses little endian b"
  },
  {
    "path": "formats/pdex.md",
    "chars": 2958,
    "preview": "The `pdex.bin` file represents information and executable code copied by `pdc` from a `pdex.elf` ELF file compiled for A"
  },
  {
    "path": "formats/pdi.md",
    "chars": 2899,
    "preview": "A file with the `.pdi` extension represents a 1-bit bitmap image that has been compiled by `pdc`. This format uses littl"
  },
  {
    "path": "formats/pds.md",
    "chars": 1541,
    "preview": "A file with the `.pds` extension represents a collection localization strings that have been compiled by `pdc`. This for"
  },
  {
    "path": "formats/pdt.md",
    "chars": 1897,
    "preview": "A file with the `.pdt` extension represents a 1-bit bitmap image table containing multiple sub-images (like a spriteshee"
  },
  {
    "path": "formats/pdv.md",
    "chars": 2533,
    "preview": "A file with the `.pdv` extension represents a 1-bit video that has been converted by `1bitvideo.app`. This format uses l"
  },
  {
    "path": "formats/pdz.md",
    "chars": 2561,
    "preview": "A file with the `.pdz` extension represents a file container that has been compiled by `pdc`. They mostly contain compil"
  },
  {
    "path": "formats/pft.md",
    "chars": 3638,
    "preview": "A file with the `.pft` extension represents a 1-bit bitmap font that has been compiled by `pdc`. This format uses little"
  },
  {
    "path": "readme.md",
    "chars": 3091,
    "preview": "Unofficial Playdate reverse-engineering notes/tools - covers file formats, server API and USB serial commands\n\n> ⚠️ This"
  },
  {
    "path": "server/api.md",
    "chars": 7248,
    "preview": "This is the API that is used by the Playdate console for things like fetching game updates, scoreboards, player details,"
  },
  {
    "path": "tools/pdex2elf.py",
    "chars": 12183,
    "preview": "import argparse\nimport hashlib\nimport zlib\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser(\n        pro"
  },
  {
    "path": "tools/pdi2png.py",
    "chars": 3790,
    "preview": "#!/usr/bin/env python3\n# PDI to PNG converter\n# PDI docs: https://github.com/jaames/playdate-reverse-engineering/blob/ma"
  },
  {
    "path": "tools/pdz.py",
    "chars": 5866,
    "preview": "# based on https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3\n# PDZ docs: https://github.com/jaames/playda"
  },
  {
    "path": "tools/usbeval.py",
    "chars": 4240,
    "preview": "if (len(argv) < 2):\n  print('usbeval.py')\n  print('Evaluates a Lua script on a Playdate device, via USB')\n  print('Requi"
  },
  {
    "path": "usb/stream.md",
    "chars": 4457,
    "preview": "# The `stream` protocol\n\nWhen `stream enable` is sent over serial, the Playdate enters streaming mode, used by Playdate "
  },
  {
    "path": "usb/usb.md",
    "chars": 12399,
    "preview": "When connected to USB and unlocked, the Playdate provides a serial interface over USB. Commands are sent as ascii and mu"
  },
  {
    "path": "usb/usb_unlocked.md",
    "chars": 6416,
    "preview": "## USB commands in unlocked mode\n\nThis is the output of running `help` on a Playdate with firmware 1.10 after running `u"
  }
]

About this extraction

This page contains the full source code of the jaames/playdate-reverse-engineering GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (94.0 KB), approximately 28.2k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!