[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: jaames\n"
  },
  {
    "path": ".gitignore",
    "content": "testing\ntools/*lua\n.obsidian"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work.\n"
  },
  {
    "path": "formats/fnt.md",
    "content": "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.\n\nEach line is one of:\n\n1. String key value data separated by an equals character `=`\n2. Glyph widths: A UTF-8 character, whitespace and pixel width of the character\n3. Kerning data: Two UTF-8 characters, whitespace and pixel offset for this kerning pair.\n4. Lua style comments beginning with `--` and empty lines which are skipped\n\n\n## Glyph Widths\n\n````\n8   8\n9   8\nspace   5\n�   9\n````\n\nIn its most basic form a fnt file is one or more lines comprising an index to specify\ncharacters and widths for each glyphs in an accompanying PNG sprite sheet.\nPairs of UTF-8 glyphs and their widths are separated by any amount of whitespace,\none per line, specified in the order they appear in the sprite sheet.\n(Left to Right, Top to Buttom)\n\nPlaydate supports all code points in the first four Unicode planes, up to U+3FFFF.\n\nBecause these glyphs and widths are whitespace separated, a special string\n `space` is substited for the ` ` space glyph.\n\n## External PNG Data\n\nThe PNG may be external file or included internally in the fnt file.\nWhen external, the accompanying PNG must be named to match the font file.\nFor example the PNG for `pantspants.fnt` would named pantspants-table-9-12.png\nassuming the fonts glyphs are 9 pixels wide and 12 pixels tall.\n\n## Internal PNG Data\n\nAlternatively the PNG data may be included within the fnt file itself, base64 encoded\nalong with the necessary metadata (height/width) required to process the PNG sprite sheet.\n\n```\ndatalen=2052\ndata=iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAAAXNSR0IArs4c6QAAAF5JREFUOE+tktsKACAIQ/X/P7owGHjJUMm3pJ02k2lei4jYy0OjyBcYyjAmQO/MnLtAOBMdQLoXZ/CIrGPquCb+1KEA4dLMsgsUceb0gCdAQPUc719eXBlc+7qH6dsbK4QPCz6OhZ4AAAAASUVORK5CYII=\n```\n| Key        | Value Detail |\n|:-----------|:-----------------------------------------|\n| `datalen=` | Integer length of `data` data value which follows (as ASCII numbers)\n| `data=`    | PNG file data, Base64 encoded\n| `width=`   | Maximum pixel width of each glyph\n| `height=`  | Maximum pixel height of each glyph\n\nA 1bit B+W PNG image contains a fixed-size sprite sheet of the individual glyphs.\nIf the glyphs themselves are narrower than the width their pixels are left justified.\n\n\n## Kerning Pairs (optional)\n\n```\nTo      -2\nTe      -4\n```\n\nKerning pairs may be specified, one line per pair with: two characters, whitespace and the offset.\n\n## Tracking info (optional)\n\n| Key        | Value Detail |\n|:-----------|:-----------------------------------------|\n| `tracking=`| Number of pixels of horizontal whitespace between glyphs within a string. Defaults to 1\n\n\n## CAPS Metadata (optional)\n\n```\n--metrics={\"baseline\":10,\"xHeight\":0,\"capHeight\":0,\"pairs\":{\"Te\":[-6,0]},\"left\":[],\"right\":[]}```\n```\n\nThe metrics line embeds a JSON object in a lua-style comment to store relevant CAPS editor metadata.\nThis data is ignored by `pdc`.\nThe [baseline](https://en.wikipedia.org/wiki/Baseline_(typography)),\n[xHeight](https://en.wikipedia.org/wiki/X-height) and\n[capHeight](https://en.wikipedia.org/wiki/Cap_height)\nare displayed while using the Caps Glyph editor, `left` and `right` contain an array of strings where\neach string includes a list of characters which have equivalent left or right-most columns and thus\nwill use identical kerning information.  The pairs object includes kerning pairs specified by the\nCaps \"Auto-Kern\" functionality.\n"
  },
  {
    "path": "formats/luac.md",
    "content": "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.\n\n### Compile Flags\n\n`#define LUA_32BITS = 1` set in `luaconf.h`.\n\n### Header Differences\n\nTODO\n\n### Opcode Differences\n\nTo 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:\n\n```\nLUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {\n/*       MM OT IT T  A  mode\t\t   opcode  */\n  opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_MOVE */\n ,opmode(0, 0, 0, 0, 1, iAsBx)\t\t/* OP_LOADI */\n ,opmode(0, 0, 0, 0, 1, iAsBx)\t\t/* OP_LOADF */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_LOADK */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_LOADKX */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_UNKNOWN */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_LOADNIL */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_GETUPVAL */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_SETUPVAL */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_GETTABUP */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_GETTABLE */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_GETI */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_GETFIELD */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_SETTABUP */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_SETTABLE */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_SETI */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_SETFIELD */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_NEWTABLE */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SELF */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_ADDI */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_ADDK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SUBK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_MULK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_MODK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_POWK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_DIVK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_IDIVK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BANDK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BORK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BXORK */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SHRI */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SHLI */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_ADD */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SUB */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_MUL */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_MOD */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_POW */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_DIV */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_IDIV */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BAND */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BOR */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BXOR */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SHL */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_SHR */\n ,opmode(1, 0, 0, 0, 0, iABC)\t\t/* OP_MMBIN */\n ,opmode(1, 0, 0, 0, 0, iABC)\t\t/* OP_MMBINI*/\n ,opmode(1, 0, 0, 0, 0, iABC)\t\t/* OP_MMBINK*/\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_UNM */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_BNOT */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_NOT */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_LEN */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_CONCAT */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_CLOSE */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_TBC */\n ,opmode(0, 0, 0, 0, 0, isJ)\t\t/* OP_JMP */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_EQ */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_LT */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_LE */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_EQK */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_EQI */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_LTI */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_LEI */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_GTI */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_GEI */\n ,opmode(0, 0, 0, 1, 0, iABC)\t\t/* OP_TEST */\n ,opmode(0, 0, 0, 1, 1, iABC)\t\t/* OP_TESTSET */\n ,opmode(0, 1, 1, 0, 1, iABC)\t\t/* OP_CALL */\n ,opmode(0, 1, 1, 0, 1, iABC)\t\t/* OP_TAILCALL */\n ,opmode(0, 0, 1, 0, 0, iABC)\t\t/* OP_RETURN */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_RETURN0 */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_RETURN1 */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_FORLOOP */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_FORPREP */\n ,opmode(0, 0, 0, 0, 0, iABx)\t\t/* OP_TFORPREP */\n ,opmode(0, 0, 0, 0, 0, iABC)\t\t/* OP_TFORCALL */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_TFORLOOP */\n ,opmode(0, 0, 1, 0, 0, iABC)\t\t/* OP_SETLIST */\n ,opmode(0, 0, 0, 0, 1, iABx)\t\t/* OP_CLOSURE */\n ,opmode(0, 1, 0, 0, 1, iABC)\t\t/* OP_VARARG */\n ,opmode(0, 0, 1, 0, 1, iABC)\t\t/* OP_VARARGPREP */\n ,opmode(0, 0, 0, 0, 0, iAx)\t\t/* OP_EXTRAARG */\n\n // opcodes appended:\n\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_LOADFALSE */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_LFALSESKIP */\n ,opmode(0, 0, 0, 0, 1, iABC)\t\t/* OP_LOADTRUE */\n};\n```\n\n## Pre SDK Version 1.8.0\n\nPrior 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.\n\nIt 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`.\n\n### Header\n\n| Offset | Type    | Detail |\n|:-------|:--------|:-------|\n| `0`    | `byte[4]` | Constant `LUA_SIGNATURE` (hex `1B 4C 75 61`) |\n| `4`    | `uint16`  | Version (`0x03F8` = 5.4.0 prerelease) |\n| `6`    | `byte`    | Constant `LUAC_FORMAT` (hex `00`) |\n| `7`    | `byte[6]` | Constant `LUAC_DATA` (hex `19 93 0D 0A 1A 0A`) |\n| `13`   | `uint8`   | Instruction size (always `4`) |\n| `14`   | `uint8`   | Integer size (always `4`) |\n| `15`   | `uint8`   | Number size (always `4`) |\n| `16`   | `lua int`  | Constant `LUA_INT` (`0x5678`) |\n| `20`   | `lua float`  | Constant `LUA_NUM` (`370.5`) |"
  },
  {
    "path": "formats/pda.md",
    "content": "A file with the `.pda` extension represents audio data that has been compiled by `pdc`. This format uses little endian byte order.\n\n## Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `char[12]` | Ident \"Playdate AUD\" |\n| `12`   | `uint24`  | Sample rate (in Hz) |\n| `15`   | `uint8`  | [Audio data format](#audio-data-format) |\n\n### Audio Data Format\n\nThe audio data format field in the file header seems to map to the `playdate.sound` constants in the official SDK:\n\n| Value | SDK Constant | Detail |\n|:------|:-------------|:-------|\n| `0`   | `kFormat8bitMono` | unsigned 8-bit PCM, one channel |\n| `1`   | `kFormat8bitStereo` | unsigned 8-bit PCM, two channels |\n| `2`   | `kFormat16bitMono` | signed 16-bit little endian PCM, one channel |\n| `3`   | `kFormat16bitStereo` | signed 16-bit little endian PCM, two channels |\n| `4`   | `kFormatADPCMMono` | 4-bit IMA ADPCM, one channel |\n| `5`   | `kFormatADPCMStereo` | 4-bit IMA ADPCM, two channels |\n\n## Audio Data\n\nThe format flag in the file header indicates how the audio is stored:\n\n### 4-bit IMA ADPCM\n\nIn 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.\n\nEach block also begins with a small header consisting of 4 bytes for each audio channel:\n\n| Type   | Detail |\n|:-------|:-------|\n| `uint16` | ADPCM predictor |\n| `uint8` | ADPCM step index |\n| `uint8` | Reserved, should always be zero |\n\nThe 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.\n\n### 8-bit PCM\n\nStandard 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.\n\n### 16-bit PCM\n\nStandard 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.\n"
  },
  {
    "path": "formats/pdex.md",
    "content": "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.\n\nSupplementary reading:\n\n- https://en.wikipedia.org/wiki/Executable_and_Linkable_Format\n- https://www.man7.org/linux/man-pages/man5/elf.5.html\n\n# File structure\n\n1. [File header](#file-header)\n2. [Program header](#program-header)\n3. [Program data](#program-data)\n   1. [Program segment](#program-segment)\n   2. [Relocation entries](#relocation-entries)\n\n## File header\n\n| Offset | Type       | Detail                         |\n|:-------|:-----------|:-------------------------------|\n| `0x00` | `char[12]` | File signature: `Playdate PDX` |\n| `0x0C` | `uint32`   | [Flags](#flags)                |\n| `0x10` | -          | End of file header (size)      |\n\n### Flags\n\n| Bitmask             | Detail                                                |\n|:--------------------|:------------------------------------------------------|\n| `flag & 0x40000000` | If `> 0`, all data after the file header is encrypted |\n\nEncryption is (at the time of writing) only used by Catalog games as a form of DRM. The encryption method is not yet known.\n\n## Program header\n\n| Offset | Type        | Detail                                             |\n|:-------|:------------|:---------------------------------------------------|\n| `0x00` | `uint8[16]` | MD5 checksum of program segment                    |\n| `0x10` | `uint32`    | Size of program segment in file image; `p_filesz`  |\n| `0x14` | `uint32`    | Size of program segment in memory image; `p_memsz` |\n| `0x18` | `uint32`    | Entry point address; `e_entry`                     |\n| `0x1C` | `uint32`    | Number of relocation entries                       |\n| `0x20` | -           | End of program header (size)                       |\n\n## Program data\n\nThe program data is zlib-compressed and consists of [a single program segment](#program-segment) immediately followed by [relocation entries](#relocation-entries).\n\n### Program segment\n\nThe 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`.\n\n### Relocation entries\n\nThe 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.\n"
  },
  {
    "path": "formats/pdi.md",
    "content": "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.\n\n## Header\n\n| Offset | Type      | Detail               |\n|:-------|:----------|:---------------------|\n| `0`    | `char[12]` | Ident `Playdate IMG` |\n| `12`   | `uint32`   | [Flags](#flags) |\n\n### Flags\n\n| Bitmask             | Detail                                      |\n|:--------------------|:--------------------------------------------|\n| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |\n\n## Image Header\n\nIf the compression flag is set, this image header follows the file header. Everything after the image header is zlib-compressed. \n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint32`  | Size of image data section when decompressed |\n| `4`    | `uint32`  | Image width (in pixels) |\n| `8`    | `uint32`  | Image height (in pixels) |\n| `12`   | `uint32`  | Unknown/reserved? Seen as 0 |\n\n## Image Data\n\n`.pdi` image data comprises of a single [Image Cell](#image-cell).\n\n## Image Cell\n\nThe `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. \n\n### Cell Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `uint16` | Cell clip width (in pixels) |\n| `2`    | `uint16` | Cell clip height (in pixels) |\n| `4`    | `uint16` | Cell stride (bytes per image row) |\n| `6`    | `uint16` | Cell clip left (in pixels) |\n| `8`    | `uint16` | Cell clip right (in pixels) |\n| `10`   | `uint16` | Cell clip top (in pixels) |\n| `12`   | `uint16` | Cell clip bottom (in pixels) |\n| `14`   | `uint16` | [Cell bitflags](#cell-bitflags) |\n\n### Cell Bitflags\n\n| Bitmask             | Detail                     |\n|:--------------------|:---------------------------|\n| `flags & 0x3` | If `> 0`, cell uses transparency |\n\n### Cell Pixels\n\nCells 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).\n\nThe 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.\n\n![Transparent edges are removed from the image to reduce its size](https://github.com/jaames/playdate-reverse-engineering/blob/main/_images/bitmap-clip.png)\n\nThe final image width will equal `clip left + clip width + clip right`, likewise the height will equal `clip top + clip height + clip bottom`,\n"
  },
  {
    "path": "formats/pds.md",
    "content": "A file with the `.pds` extension represents a collection localization strings that have been compiled by `pdc`. This format uses little endian byte order.\n\n## Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `char[12]` | Ident \"Playdate STR\" |\n| `12`   | `uint32`   | [Flags](#flags) |\n\n### Flags\n\n| Bitmask             | Detail                                      |\n|:--------------------|:--------------------------------------------|\n| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |\n\n### String header\n\nIf the compression flag is set, there's an extra string data header after the file header. Everything after this is zlib-compressed. \n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`   | `uint32`  | Size of decompressed string data |\n| `4`   | `uint32`  | Unused/reserved, seen as 0 |\n| `8`   | `uint32`  | Unused/reserved, seen as 0 |\n| `12`  | `uint32`  | Unused/reserved, seen as 0 |\n\n## String Data\n\n### Table Header\n\n| Offset | Type    | Detail |\n|:-------|:--------|:-------|\n| `0`    | `uint32` | Number of string entries |\n\n### Table\n\nAfter 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.\n\nOffsets are relative to the end of the table, and the first string entry always begins directly after the table.\n\n### String Entries\n\nEach string entry contains an utf8 string key, followed by a null byte, followed by a utf8 string value, followed by another null byte."
  },
  {
    "path": "formats/pdt.md",
    "content": "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.\n\n## Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `char[12]` | Ident `Playdate IMT` |\n| `12`   | `uint32`   | [Flags](#flags) |\n\n### Flags\n\n| Bitmask             | Detail                                      |\n|:--------------------|:--------------------------------------------|\n| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |\n\n## Image Header\n\nIf the compression flag is set, there's an extra header after the file header:\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint32`  | Size of decompressed image data |\n| `4`    | `uint32`  | Image width (in pixels) |\n| `8`    | `uint32`  | Image height (in pixels) |\n| `12`   | `uint32`  | Number of cells |\n\nThe image width and height are for the first image only. In sequential image tables, the following images may be of\ndifferent sizes. In matrix image tables, all images must be the same size.\n\nIf the compression flag is set, then this section is zlib-compressed.\n\n## Image Data\n\n### Table Header\n\n| Offset | Type    | Detail |\n|:-------|:--------|:-------|\n| `0`    | `uint16` | Num cells |\n| `2`    | `uint16` | Num cells per row |\n\nFor 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.\n\n### Table\n\nAfter 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.\n\nOffsets are relative to the end of the table, and the first cell always begins directly after the table.\n\n### Image Cell\n\nSee: [Image Cell](/formats/pdi.md#image-cell)\n"
  },
  {
    "path": "formats/pdv.md",
    "content": "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.\n\n## Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `char[12]` | Ident `Playdate VID` |\n| `12`   | `uint32`   | Reserved, always 0  |\n| `16`   | `uint16` | Number of frames |\n| `18`   | `uint16` | Reserved, always 0 |\n| `20`   | `float32` | Framerate, measured in frames per second |\n| `24`   | `uint16` | Frame width (in pixels) |\n| `26`   | `uint16` | Frame height (in pixels) |\n\nIn 1bitvideo.app the frame width and height seem to be hardcoded to `400` and `240` respectively, at least at the time of writing.\n\n## Frame Table\n\nFollowing 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:\n\n| Value | Detail |\n|:------|:-------|\n| `value >> 2` | Offset |\n| `value & 0x3` | Frame type |\n\n### Frame Types\n\n| Type | Detail |\n|:-----|:-------|\n| `0`  | No frame |\n| `1`  | [I-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |\n| `2`  | [P-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |\n| `3`  | Combined I-frame and P-frame |\n\nA `0` type frame is placed at the end to identify where the preceeding frame's data ends. There is no actual data following it.\n\n## Frame Data\n\nFrame 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.\n\n### P-frames\n\nFrame 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.\n\nFor example in C this would be something like:\n\n```c\nfor (int i = 0; i < sizeof(frame); i++)\n{\n  frame[i] ^= prevFrame[i];\n}\n```\n\n### Combined I-frame and P-frame\n\nFrame 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. \n\nThe 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.\n"
  },
  {
    "path": "formats/pdz.md",
    "content": "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.\n\n## Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:-------|\n| `0`    | `char[12]` | Ident `Playdate PDZ` |\n| `12`   | `uint32`   | [Flags](#flags)  |\n\n### Flags\n\n| Bitmask             | Detail                                      |\n|:--------------------|:--------------------------------------------|\n| `flag & 0x40000000` | If `> 0`, the data in this file is encrypted |\n\nFile 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.\n\n## File Entries\n\nFollowing the header is a list of file entries. Each entry has a header.\n\n| Type    | Detail |\n|:--------|:-------|\n| `uint8`  | [Entry Flags](#entry-flags) |\n| `uint24` | [Entry Data](#entry-data) length |\n| `string` | Filename as null-terminated C string |\n| `-` | Optional null-padding if needed to align to the next multiple of 4 bytes |\n\nIf the [Entry Type](#entry-type) flag is `5` (for a `.pda` audio file), some additional values are included:\n\n| Type    | Detail |\n|:--------|:-------|\n| `uint24` | Audio sample rate in Hz |\n| `uint8`  | [Audio Data Format](/format/pda.md#audio-data-format) |\n\n### Entry Flags\n\n| Flag | Detail |\n|:-------|:-------|\n| `flags & 0x80` | If `> 0`, file entry data is compressed |\n| `flags & 0x7F` | [Entry Type](#entry-type) |\n\n### Entry Type\n\n| Flag | Detail |\n|:-------|:-------|\n| `0` | Unknown/unused |\n| `1` | Compiled Lua bytecode ([`.luac`](/formats/luac.md)) |\n| `2` | Static image ([`.pdi`](/formats/pdi.md)) |\n| `3` | Animated image ([`.pdt`](/formats/pdt.md)) |\n| `4` | Video ([`.pdv`](/formats/pdv.md)) |\n| `5` | Audio ([`.pda`](/formats/pda.md)) |\n| `6` | Text strings ([`.pds`](/formats/pds.md)) |\n| `7` | Font ([`.pft`](/formats/pft.md)) |\n\n## Entry Data\n\nThe 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.\n\nAll 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."
  },
  {
    "path": "formats/pft.md",
    "content": "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.\n\n## Header\n\n| Offset | Type      | Detail               |\n|:-------|:----------|:---------------------|\n| `0`    | `char[12]` | Ident `Playdate FNT` |\n| `12`   | `uint32`   | [Flags](#flags) |\n\n### Flags\n\n| Bitmask             | Detail                                      |\n|:--------------------|:--------------------------------------------|\n| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |\n| `flags & 0x00000001` | If `> 0`, the font contains characters above U+1FFFF |\n\n## Font Header\n\nIf the compression flag is set, there's an extra font header after the file header. Everything after this is zlib-compressed. \n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint32`  | Size of font data section when decompressed |\n| `4`    | `uint32`  | Maximum glyph width (in pixels) |\n| `8`    | `uint32`  | Maximum glyph height (in pixels) |\n\n## Page List\n\n### Page List Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint8`  | Glyph width (in pixels) |\n| `1`    | `uint8`  | Glyph height (in pixels) |\n| `2`    | `uint16`  | Tracking (in pixels) |\n| `4`    | `64 bytes` | [Page Usage Flags](#page-usage-flags) |\n\nFont 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.\n\nThe page index for a given glyph codepoint will be `codepoint >> 8`.\n\nFollowing this header is a list of `uint32` offsets for all of the pages present in the file, with the pages following immediately after.\n\n### Page Usage Flags\n\nThis 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.\n\n## Page\n\n### Page Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint24`  | Reserved? Seen as `0` |\n| `3`    | `uint8`  | Number of glyphs |\n| `4`    | `32 bytes`  | [Glyph Usage Flags](#glyph-usage-flags) |\n\nAfter the page header is a series of [Glyphs](#glyph) for the page.\n\n### Glyph Usage Flags\n\nThis 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.\n\n## Glyph\n\nEach glyph is comprised of:\n - a header\n - a short kerning table\n - (if necessary) padding bytes to align to the next multiple of 4\n - a long kerning table\n - pixel data\n\n### Glyph Header\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint8`  | Glyph advance / width (in pixels) |\n| `1`    | `uint8`  | Number of [Short Kerning Table Entries](#short-kerning-table-entries) |\n| `2`    | `uint16`  | Number of [Long Kerning Table Entries](#long-kerning-table-entries) |\n\n### Short Kerning Table Entries\n\nI think this is for codepoints within the same page?\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint8`  | Other glyph codepoint |\n| `1`    | `int8`  | Kerning (in pixels) |\n\n### Long Kerning Table Entries\n\nThis supports any unicode codepoint within the whole font\n\n| Offset | Type     | Detail |\n|:-------|:---------|:--------------------------------|\n| `0`    | `uint24`  | Other glyph codepoint |\n| `3`    | `int8`  | Kerning (in pixels) |\n\n### Glyph pixels\n\nStored as an [Image Cell](/formats/pdi.md#image-cell)."
  },
  {
    "path": "readme.md",
    "content": "Unofficial Playdate reverse-engineering notes/tools - covers file formats, server API and USB serial commands\n\n> ⚠️ 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. \n\n## Documentation\n\n- **File Formats**\n  - **Playdate game formats**\n    - [**pdex.bin**](formats/pdex.md) - Executable code\n    - [**.luac**](formats/luac.md) - Lua bytecode\n    - [**.pdz**](formats/pdz.md) - File container\n    - [**.pda**](formats/pda.md) - Audio file\n    - [**.pdi**](formats/pdi.md) - Image file\n    - [**.pdt**](formats/pdt.md) - Imagetable file\n    - [**.pdv**](formats/pdv.md) - Video file\n    - [**.pds**](formats/pds.md) - Strings file\n    - [**.pft**](formats/pft.md) - Font file\n  - **Other formats**\n    - [**.fnt**](formats/fnt.md) - Font source file\n    - **.strings** - Strings source file (TODO)\n- **Server**\n  - [**Playdate API**](server/api.md) - Main Playdate server API\n- **Misc**\n  - [**USB**](usb/usb.md) - USB serial interface\n  - [**Streaming**](usb/stream.md) - Video/audio streaming protocol (via USB serial), used by Playdate Mirror\n\n## Tools\n\n- [**`pdz.py`**](tools/pdz.py) - Unpacks all files from a `.pdz` file container.\n- [**`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`.\n- [**`pdi2png.py`**](tools/pdi2png.py) - Converts a `.pdi` image file to a `.png` image.\n- [**`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.\n\n## Related Projects and Resources\n\n- [**pd-usb**](https://github.com/jaames/pd-usb) - JavaScript library for interacting with the Playdate's serial API from a WebUSB-compatible web browser.\n- [**unluac**](https://github.com/scratchminer/unluac) - Fork of the unluac Lua decompiler, modified to support Playdate-flavoured Lua.\n- [**lua54**](https://github.com/scratchminer/lua54) - Fork of Lua that aims to match the custom tweaks that Panic added for Playdate-flavoured Lua.\n\n## Special Thanks\n\n - [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`\n - [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).\n - [Simon](https://github.com/simontime) for helping with some ADPCM audio data reverse engineering\n - The folks at [Panic](https://panic.com/) for making such a wonderful and fascinating handheld!\n\n ----\n\n 2022-2023 James Daniel\n\n Playdate is © [Panic Inc.](https://panic.com/) - this project isn't affiliated with or endorsed by them in any way.\n"
  },
  {
    "path": "server/api.md",
    "content": "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).\n\nThis 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.\n\n## Endpoints\n\n| Method | Path |\n|:-|:-|\n| `POST` | [`/auth_echo/`](#post-auth_echo) |\n| `GET`  | [`/player/`](#get-player) |\n| `GET`  | [`/player/:playerId/`](#get-playerplayerid) |\n| `POST` | `/player/avatar/` |\n| `GET`  | [`/games/scheduled/`](#get-gamesscheduled) |\n| `GET`  | [`/games/user/`](#get-gamesuser) |\n| `GET`  | `/games/testing/` |\n| `GET`  | [`/games/system/`](#get-gamessystem) |\n| `GET`  | [`/games/purchased/`](#get-gamespurchased) |\n| `GET`  | [`/games/catalog`](#get-gamescatalog) |\n| `GET`  | [`/games/catalog/:idx`](#get-gamescatalogidx) |\n| `POST`  | [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |\n| `POST`  | [`/games/:bundleId/purchase/confirm`](#post-gamesbundleidpurchaseconfirm) |\n| `GET`  | `/games/:bundleId/latest_build/` |\n| `GET`  | `/games/:bundleId/boards/` |\n| `GET`  | `/games/:bundleId/boards/:boardId/` |\n| `POST` | `/games/:bundleId/boards/:boardId/` |\n| `GET`  | `/device/settings/` |\n\n### POST /auth_echo\n\nSeems to just return whatever JSON body is sent to it.\n\n### GET /player\n\nReturns the player profile for the user that owns the current access token.\n\n### GET /player/:playerId\n\nSame as `/player`, but gets the player profile for another user, given their [Player ID](#player-id).\n\n### GET /games/scheduled\n\nReturns an array of [Schedule](#Schedule) entries for any seasons that you have access to.\n\n### GET /games/user\n\nReturns an array of [Game](#Game) entries for games that you have [sideloaded](https://help.play.date/games/sideloading/).\n\n### GET /games/system\n\nReturns an array of [Game](#Game) entries for additional system applications, such as [Catalog](https://play.date/games/catalog/).\n\n### GET /games/purchased\n\nReturns an array of [Game](#Game) entries for games that you have purchased through [Catalog](https://play.date/games/catalog/).\n\n### GET /games/catalog\n\nReturns an array of [Catalog Game](#CatalogGame) entries for games that are available through [Catalog](https://play.date/games/catalog/).\n\n### GET /games/catalog/:idx\n\nReturns [Catalog Game](#CatalogGame) entry for a specific [Catalog](https://play.date/games/catalog/) game.\n\n### POST /games/:bundleId/purchase/\n\nInitiates the purchase flow for a game. Returns instructions for confirming the purchase on another device.\n\n### POST /games/:bundleId/purchase/confirm\n\nCompletes the purchase flor for a game.\n\n### GET /device/register/:serialNumber\n\nIf the device hasn't already been registered, returns a JSON containing its serial number and pin.\n\nThis endpoint requires an extra header:\n\n| Header | Value |\n|:-|:-|\n| `Idempotency-Key` | Random 16-character string. Allowed chars are `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`. |\n\n### GET /device/register/:serialNumber/complete\n\nReturns 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.\n\n## Schemas\n\n### Schedule\n\n| Key | Type | Detail |\n|:----|:------|:------|\n| `name` | string | Schedule name (Season One is `Season-001`) |\n| `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`) |\n| `start_date_timestamp` | number | Schedule start as a UNIX timestamp |\n| `next_release_timestamp` | number | Time of next scheduled release as a UNIX timestamp, can be `null` |\n| `ended` | boolean | |\n| `games` | array | Array of available [Game](#Game) entries |\n\n### Game \n\n| Key | Type | Detail |\n|:----|:------|:------|\n| `name` | string | Game name, will be displayed to the user |\n| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |\n| `short_description` | string | Few games currently have this (only seen on Flipper Lifter and Boogie Loops so far), often `null` |\n| `studio` | string | Game's publisher/developer |\n| `has_newer_build` | boolean | |\n| `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. |\n| `latest_build` | [Build](#Build) | |\n\n### Build\n\n| Key | Type | Detail |\n|:----|:------|:------|\n| `url` | string | Web URL for the build's .zip file |\n| `is_beta` | boolean | |\n| `version` | string | Human-friendly version string, taken from the game's pdxinfo file |\n| `build_number` | number | Incremental build number from the game's pdxinfo |\n| `filesize` | number | .zip file size, in bytes |\n| `upzipped_filesize` | number | Size of the .zip contents after decompression, in bytes |\n\n### Catalog Game\n\n| Key | Type | Detail |\n|:----|:------|:------|\n| `name` | string | Game name, will be displayed to the user |\n| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |\n| `studio` | string | Game's publisher/developer |\n| `description` | string | Game's description |\n| `detail_url` | string | Path for this game's [`/games/catalog/:idx`](#get-gamescatalogidx) endpoint |\n| `price` | number | Price in USD |\n| `header_image` | string | Path to .pdi image file |\n| `list_image_size` | string | `\"small\"` |\n| `list_image` | string | Path to .pdi image file |\n| `animation_frame_duration` | unknown | Seen as `null` |\n| `animation_frame_timing` | unknown | Seen as `null` |\n| `accessibility` | string | Game accessibility information |\n| `rating` | string | Game age rating |\n| `screenshots` | [Catalog Screenshot](#CatalogScreenshot)[] | Game screenshots |\n| `build_size` | string | Download size, e.g. `\"22.8 MB\"` |\n| `published_date` | string | |\n| `updated_date` | string | |\n| `authorized` | boolean | |\n| `purchasable` | boolean | |\n| `short_description` | string | |\n| `web_url` | string | URL for this game on the Catalog web storefront |\n| `purchase_url` | string | Path for this game's [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |\n\n### Catalog Screenshot\n\n| Key | Type | Detail |\n|:----|:------|:------|\n| `url` | string | Path to .pdi image file |\n| `frame_timing` | number[] | |\n\n## Auth Headers\n\nAll routes require a basic authorization token sent via a HTTP header. \n\n| Header | Value |\n|:-|:-|\n| `Authorization` | `Token ` followed by your authorization token |\n\n### Simulator Tokens\n\nIf 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.\n\n## Player ID\n\nPlayer 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`.\n"
  },
  {
    "path": "tools/pdex2elf.py",
    "content": "import argparse\nimport hashlib\nimport zlib\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser(\n        prog='pdex2elf.py',\n        description='Converts a Playdate pdex.bin file to an ELF file.'\n    )\n    parser.add_argument('pdex', help='the path to the input pdex.bin')\n    parser.add_argument('elf', help='the path to the output ELF file')\n    args = parser.parse_args()\n\n    with open(args.pdex, 'rb') as pdex:\n        pdex_magic = pdex.read(12)\n        if pdex_magic in [b'Playdate PDX', b'Playdate BIN']:\n            pdex_flags = int.from_bytes(pdex.read(4), byteorder='little')\n            if pdex_flags & 0x40000000:\n                raise ValueError('the specified pdex.bin is encrypted')\n            pdex_version = '2.0'\n            pdex_checksum = pdex.read(16)\n            pdex_filesz = int.from_bytes(pdex.read(4), byteorder='little')\n            pdex_memsz = int.from_bytes(pdex.read(4), byteorder='little')\n            pdex_entry = int.from_bytes(pdex.read(4), byteorder='little')\n            pdex_relnum = int.from_bytes(pdex.read(4), byteorder='little')\n            pdex_data = zlib.decompress(pdex.read())\n        else:\n            pdex_entry = int.from_bytes(pdex_magic[:4], byteorder='little') - 0x6000000c\n            pdex_filesz = int.from_bytes(pdex_magic[4:8], byteorder='little') - 0x6000000c\n            pdex_memsz = int.from_bytes(pdex_magic[8:], byteorder='little') - 0x6000000c\n            if pdex_entry < 0 or pdex_filesz < 0 or pdex_memsz < 0:\n                raise ValueError('the specified file is not a pdex.bin')\n            pdex_version = '1.0'\n            pdex_magic = None\n            pdex_flags = 0\n            pdex_relnum = 0\n            pdex_checksum = None\n            pdex_data = pdex.read()\n\n    print('pdex.bin info:')\n    print('  Version:           {}'.format(pdex_version))\n    if pdex_magic is not None:\n        print('  File signature:    {}'.format(pdex_magic.decode()))\n    print('  Flags:             {}'.format(pdex_flags))\n    if pdex_checksum is not None:\n        print('  Declared checksum: {}'.format(pdex_checksum.hex()))\n        print('  Computed checksum: {}'.format(hashlib.md5(pdex_data[:pdex_filesz]).hexdigest()))\n    print('  Entry point:       {}'.format(pdex_entry))\n    print('  File size:         {}'.format(pdex_filesz))\n    print('  Memory size:       {}'.format(pdex_memsz))\n    print('  Relocations:       {}'.format(pdex_relnum))\n\n    with open(args.elf, 'wb') as elf:\n        text_index = 1\n        text_addr = 0\n        text_offset = 0x10000\n        text_size = pdex_filesz\n\n        bss_index = 2\n        bss_addr = (pdex_filesz + 3) & ~3\n        bss_offset = text_offset + bss_addr\n        bss_size = pdex_memsz - pdex_filesz\n\n        rel_text_index = 3\n        rel_text_offset = bss_offset\n        rel_text_size = pdex_relnum * 8\n\n        symtab_index = 4\n        symtab_offset = (rel_text_offset + rel_text_size + 3) & ~3\n        symtab_size = 2 * 16\n\n        strtab_index = 5\n        strtab_data = b'\\0'\n        strtab_offset = symtab_offset + symtab_size\n        strtab_size = len(strtab_data)\n\n        shstrtab_index = 6\n        shstrtab_data = b'\\0.text\\0.bss\\0.rel.text\\0.symtab\\0.strtab\\0.shstrtab\\0'\n        shstrtab_offset = strtab_offset + strtab_size\n        shstrtab_size = len(shstrtab_data)\n\n        sh_offset = (shstrtab_offset + shstrtab_size + 3) & ~3\n\n        # ==== ELF header ====\n\n        # e_ident[EI_MAG0..EI_MAG3]\n        elf.write(b'\\x7fELF')\n        # e_ident[EI_CLASS]\n        elf.write(b'\\x01') # ELFCLASS32\n        # e_ident[EI_DATA]\n        elf.write(b'\\x01') # ELFDATA2LSB\n        # e_ident[EI_VERSION]\n        elf.write(b'\\x01') # EV_CURRENT\n        # e_ident[EI_OSABI]\n        elf.write(b'\\x00') # ELFOSABI_SYSV\n        # e_ident[EI_ABIVERSION]\n        elf.write(b'\\x00')\n        # e_ident[EI_PAD..(EI_NIDENT - 1)]\n        elf.write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00')\n        # e_type\n        elf.write(b'\\x02\\x00') # ET_EXEC\n        # e_machine\n        elf.write(b'\\x28\\x00') # EM_ARM\n        # e_version\n        elf.write(b'\\x01\\x00\\x00\\x00') # EV_CURRENT\n        # e_entry\n        elf.write(pdex_entry.to_bytes(4, byteorder='little'))\n        # e_phoff\n        elf.write(b'\\x34\\x00\\x00\\x00')\n        # e_shoff\n        elf.write(sh_offset.to_bytes(4, byteorder='little'))\n        # e_flags\n        elf.write(b'\\x00\\x04\\x00\\x05') # EF_ARM_EABI_VER5 | EF_ARM_ABI_FLOAT_HARD\n        # e_ehsize\n        elf.write(b'\\x34\\x00')\n        # e_phentsize\n        elf.write(b'\\x20\\x00')\n        # e_phnum\n        elf.write(b'\\x01\\x00')\n        # e_shentsize\n        elf.write(b'\\x28\\x00')\n        # e_shnum\n        elf.write(b'\\x07\\x00')\n        # e_shstrndx\n        elf.write(shstrtab_index.to_bytes(2, byteorder='little'))\n\n        # ==== Program header ====\n\n        # p_type\n        elf.write(b'\\x01\\x00\\x00\\x00') # PT_LOAD\n        # p_offset\n        elf.write(text_offset.to_bytes(4, byteorder='little'))\n        # p_vaddr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # p_paddr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # p_filesz\n        elf.write(pdex_filesz.to_bytes(4, byteorder='little'))\n        # p_memsz\n        elf.write(pdex_memsz.to_bytes(4, byteorder='little'))\n        # p_flags\n        elf.write(b'\\x07\\x00\\x00\\x00') # PF_X | PF_W | PF_R\n        # p_align\n        elf.write(text_offset.to_bytes(4, byteorder='little'))\n\n        # ==== .text section ====\n\n        elf.write(b'\\x00' * (text_offset - elf.tell()))\n\n        elf.write(pdex_data[:pdex_filesz])\n\n        # ==== .rel.text section ====\n\n        elf.write(b'\\x00' * (rel_text_offset - elf.tell()))\n\n        for i in range(pdex_filesz, pdex_filesz + 4 * pdex_relnum, 4):\n            # r_offset\n            elf.write(pdex_data[i:(i + 4)])\n            # r_info\n            elf.write(b'\\x02')\n            elf.write(text_index.to_bytes(2, byteorder='little'))\n            elf.write(b'\\x00')\n\n        # ==== .symtab section ====\n\n        elf.write(b'\\x00' * (symtab_offset - elf.tell()))\n\n        # NULL\n        # st_name\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # st_value\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # st_size\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # st_info\n        elf.write(b'\\x00') # STB_LOCAL, STT_NOTYPE\n        # st_other\n        elf.write(b'\\x00')\n        # st_shndx\n        elf.write(b'\\x00\\x00')\n\n        # .text\n        # st_name\n        elf.write(text_addr.to_bytes(4, byteorder='little'))\n        # st_value\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # st_size\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # st_info\n        elf.write(b'\\x03') # STB_LOCAL, STT_SECTION\n        # st_other\n        elf.write(b'\\x00')\n        # st_shndx\n        elf.write(text_index.to_bytes(2, byteorder='little'))\n\n        # ==== .strtab section ====\n\n        elf.write(strtab_data)\n\n        # ==== .shstrtab section ====\n\n        elf.write(shstrtab_data)\n\n        # ==== Section headers ====\n\n        elf.write(b'\\x00' * (sh_offset - elf.tell()))\n\n        # NULL\n        # sh_name\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_type\n        elf.write(b'\\x00\\x00\\x00\\x00') # SHT_NULL\n        # sh_flags\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_offset\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_size\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_link\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_info\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x00\\x00\\x00\\x00')\n\n        # .text\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.text\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x01\\x00\\x00\\x00') # SHT_PROGBITS\n        # sh_flags\n        elf.write(b'\\x37\\x00\\x00\\x00') # SHF_WRITE | SHF_ALLOC | SHF_EXECINSTR | SHF_MERGE | SHF_STRINGS\n        # sh_addr\n        elf.write(text_addr.to_bytes(4, byteorder='little'))\n        # sh_offset\n        elf.write(text_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(text_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_info\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x08\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x00\\x00\\x00\\x00')\n\n        # .bss\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.bss\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x08\\x00\\x00\\x00') # SHT_NOBITS\n        # sh_flags\n        elf.write(b'\\x03\\x00\\x00\\x00') # SHF_WRITE | SHF_ALLOC\n        # sh_addr\n        elf.write(bss_addr.to_bytes(4, byteorder='little'))\n        # sh_offset\n        elf.write(bss_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(bss_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_info\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x04\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x00\\x00\\x00\\x00')\n\n        # .rel.text\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.rel.text\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x09\\x00\\x00\\x00') # SHT_REL\n        # sh_flags\n        elf.write(b'\\x40\\x00\\x00\\x00') # SHF_INFO_LINK\n        # sh_addr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_offset\n        elf.write(rel_text_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(rel_text_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(symtab_index.to_bytes(4, byteorder='little'))\n        # sh_info\n        elf.write(text_index.to_bytes(4, byteorder='little'))\n        # sh_addralign\n        elf.write(b'\\x04\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x08\\x00\\x00\\x00')\n\n        # .symtab\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.symtab\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x02\\x00\\x00\\x00') # SHT_SYMTAB\n        # sh_flags\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_offset\n        elf.write(symtab_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(symtab_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(strtab_index.to_bytes(4, byteorder='little'))\n        # sh_info\n        elf.write(b'\\x02\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x04\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x10\\x00\\x00\\x00')\n\n        # .strtab\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.strtab\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x03\\x00\\x00\\x00') # SHT_STRTAB\n        # sh_flags\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_offset\n        elf.write(strtab_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(strtab_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_info\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x01\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x00\\x00\\x00\\x00')\n\n        # .shstrtab\n        # sh_name\n        elf.write((shstrtab_data.index(b'\\0.shstrtab\\0') + 1).to_bytes(4, byteorder='little'))\n        # sh_type\n        elf.write(b'\\x03\\x00\\x00\\x00') # SHT_STRTAB\n        # sh_flags\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addr\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_offset\n        elf.write(shstrtab_offset.to_bytes(4, byteorder='little'))\n        # sh_size\n        elf.write(shstrtab_size.to_bytes(4, byteorder='little'))\n        # sh_link\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_info\n        elf.write(b'\\x00\\x00\\x00\\x00')\n        # sh_addralign\n        elf.write(b'\\x01\\x00\\x00\\x00')\n        # sh_entsize\n        elf.write(b'\\x00\\x00\\x00\\x00')\n\n    print('')\n    print(\"ELF file successfully written to '{}'\".format(args.elf))\n"
  },
  {
    "path": "tools/pdi2png.py",
    "content": "#!/usr/bin/env python3\n# PDI to PNG converter\n# PDI docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdi.md\n\nfrom struct import unpack, pack\nfrom zlib import compress, crc32, decompress\nfrom argparse import ArgumentParser\n\nPDI_IDENT = b'Playdate IMG'\nPNG_SIGNATURE = b'\\x89PNG\\r\\n\\x1a\\n'\n\ndef png_chunk(chunk_type, data):\n  chunk = chunk_type + data\n  return pack('>I', len(data)) + chunk + pack('>I', crc32(chunk) & 0xffffffff)\n\ndef write_png(path, width, height, rows, has_alpha):\n  # color type: 0 = grayscale, 4 = grayscale+alpha\n  color_type = 4 if has_alpha else 0\n  ihdr = pack('>IIBBBBB', width, height, 8, color_type, 0, 0, 0)\n\n  # build raw image data: filter byte (0=none) + row pixels\n  raw = bytearray()\n  for row in rows:\n    raw.append(0)  # filter: none\n    raw.extend(row)\n\n  with open(path, 'wb') as f:\n    f.write(PNG_SIGNATURE)\n    f.write(png_chunk(b'IHDR', ihdr))\n    f.write(png_chunk(b'IDAT', compress(bytes(raw))))\n    f.write(png_chunk(b'IEND', b''))\n\ndef read_cell(data, offset):\n  clip_width, clip_height, stride, clip_left, clip_right, clip_top, clip_bottom, flags = \\\n    unpack('<8H', data[offset:offset + 16])\n  offset += 16\n\n  has_alpha = (flags & 0x3) > 0\n\n  # read color bitmap (1-bit, 0=black 1=white)\n  color_size = stride * clip_height\n  color_data = data[offset:offset + color_size]\n  offset += color_size\n\n  # read alpha bitmap if present (1-bit, 0=transparent 1=opaque)\n  alpha_data = None\n  if has_alpha:\n    alpha_data = data[offset:offset + color_size]\n    offset += color_size\n\n  # reconstruct full image dimensions\n  full_width = clip_left + clip_width + clip_right\n  full_height = clip_top + clip_height + clip_bottom\n\n  # build pixel rows\n  rows = []\n  for y in range(full_height):\n    if has_alpha:\n      row = bytearray(full_width * 2)  # grayscale + alpha per pixel\n    else:\n      row = bytearray(b'\\xff' * full_width)  # default white\n\n    cy = y - clip_top\n    if 0 <= cy < clip_height:\n      row_offset = cy * stride\n      for x in range(clip_width):\n        byte_index = row_offset + (x // 8)\n        bit_index = 7 - (x % 8)\n        color_bit = (color_data[byte_index] >> bit_index) & 1\n        color = 255 if color_bit else 0\n        px = clip_left + x\n\n        if has_alpha:\n          alpha_bit = (alpha_data[byte_index] >> bit_index) & 1\n          alpha = 255 if alpha_bit else 0\n          row[px * 2] = color\n          row[px * 2 + 1] = alpha\n        else:\n          row[px] = color\n\n    rows.append(row)\n\n  return full_width, full_height, rows, has_alpha\n\ndef convert_pdi(input_path, output_path):\n  with open(input_path, 'rb') as f:\n    data = f.read()\n\n  # verify ident\n  ident = data[0:12]\n  if ident != PDI_IDENT:\n    raise ValueError(f'Not a valid PDI file (got ident {ident!r})')\n\n  flags = unpack('<I', data[12:16])[0]\n  is_compressed = (flags & 0x80000000) > 0\n\n  offset = 16\n\n  if is_compressed:\n    decompressed_size, width, height, reserved = unpack('<4I', data[offset:offset + 16])\n    offset += 16\n    image_data = decompress(data[offset:])\n  else:\n    image_data = data[offset:]\n\n  full_width, full_height, rows, has_alpha = read_cell(image_data, 0)\n  write_png(output_path, full_width, full_height, rows, has_alpha)\n  print(f'Saved {output_path} ({full_width}x{full_height})')\n\nif __name__ == '__main__':\n  parser = ArgumentParser(description='Convert Playdate .pdi images to .png')\n  parser.add_argument('input', help='Input .pdi file path')\n  parser.add_argument('-o', '--output', help='Output .png file path (default: input with .png extension)')\n  args = parser.parse_args()\n\n  output = args.output\n  if not output:\n    if args.input.lower().endswith('.pdi'):\n      output = args.input[:-4] + '.png'\n    else:\n      output = args.input + '.png'\n\n  convert_pdi(args.input, output)\n"
  },
  {
    "path": "tools/pdz.py",
    "content": "# based on https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3\n# PDZ docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdz.md\n\nfrom sys import exit\nfrom os import path, makedirs\nfrom struct import pack, unpack\nfrom zlib import decompress\nfrom argparse import ArgumentParser\n\nPDZ_IDENT = b'Playdate PDZ'\nFILE_TYPES = {\n  1: 'luac',\n  2: 'pdi',\n  3: 'pdt',\n  4: 'pdv',\n  5: 'pda',\n  6: 'pds',\n  7: 'pft',\n}\nFILE_IDENTS = {\n  'pdi': b'Playdate IMG',\n  'pdt': b'Playdate IMT',\n  'pdv': b'Playdate VID',\n  'pda': b'Playdate AUD',\n  'pds': b'Playdate STR',\n  'pft': b'Playdate FNT'\n}\n\nclass PlaydatePdz:\n  @classmethod\n  def open(cls, path):\n    with open(path, \"rb\") as buffer:\n      return cls(buffer)\n\n  def __init__(self, buffer):\n    self.buffer = buffer\n    self.entries = {}\n    self.num_entries = 0\n    self.read_header()\n    self.read_entries()\n\n  def read_header(self):\n    self.buffer.seek(0)\n    magic = self.buffer.read(16)\n    magic = magic[:magic.index(b'\\0')] # trim null bytes\n    assert magic == PDZ_IDENT, 'Invalid PDZ file ident'\n    self.buffer.seek(12)\n    flags = unpack('<I', self.buffer.read(4))[0]\n    is_encrypted = (flags & 0x40000000) > 0\n    assert not is_encrypted, 'PDZ file is encrypted'\n\n  def read_string(self):\n    res = b''\n    while True:\n      char = self.buffer.read(1)\n      if char == b'\\0': break\n      res += char\n    return res.decode()\n\n  def read_entries(self):\n    self.buffer.seek(0, 2)\n    ptr = 0x10\n    pdz_len = self.buffer.tell()\n    self.buffer.seek(ptr)\n    while ptr < pdz_len:\n      head = unpack('<I', self.buffer.read(4))[0]\n      flags = head & 0xFF\n      entry_len = (head >> 8) & 0xFFFFFF\n      # doesn't seem to be any other flags\n      is_compressed = (flags >> 7) & 0x1\n      file_type = FILE_TYPES[flags & 0xF]\n      # file name is a null terminated string\n      file_name = self.read_string()\n      # align offset to next nearest multiple of 4\n      self.buffer.seek((self.buffer.tell() + 3) & ~3)\n      # .pda files have two more values after filename before data\n      if file_type == 'pda':\n        entry_len -= 4\n        audio_info = unpack('<I', self.buffer.read(4))[0]\n        audio_rate = audio_info & 0xFFFFFF\n        audio_format = (audio_info >> 24) & 0xFF \n      # if compression flag is set, there's another uint32 with the decompressed size\n      if is_compressed:\n        decompressed_size = unpack('<I', self.buffer.read(4))[0]\n        entry_len -= 4\n      else:\n        decompressed_size = entry_len\n\n      data = self.buffer.read(entry_len)\n      ptr = self.buffer.tell()\n      \n      self.num_entries += 1\n      self.entries[file_name] = {\n        'name': file_name,\n        'type': file_type,\n        'data': data,\n        'size': entry_len,\n        'compressed': is_compressed,\n        'decompressed_size': decompressed_size\n      }\n      if file_type == 'pda':\n        self.entries[file_name].update({\n          'audio_rate': audio_rate, \n          'audio_format': audio_format})\n  \n  def get_entry_data(self, name):\n    assert name in self.entries\n    entry = self.entries[name]\n    if entry['compressed']:\n      return decompress(entry['data'])\n    return entry['data']\n  \n  def construct_entry_header(self, name):\n    # this is probably incorrect, use at your own risk\n    assert name in self.entries\n    entry = self.entries[name]\n    file_type = entry['type']\n    is_compressed = entry['compressed']\n    assert file_type in ['pdi','pdt','pdv','pda','pds','pft']\n    ident = FILE_IDENTS[file_type]\n    if file_type == 'pda':\n      rate = entry['audio_rate']\n      fmt = entry['audio_format']\n      audio_info = (fmt << 24) + rate\n      header = pack('<12sI', ident, audio_info)\n    else:\n      flags = 0x80000000 if is_compressed else 0x00000000\n      header = pack('<12sI', ident, flags)\n    return header\n\n  def save_entry_data(self, name, outdir, gen_header):\n    assert name in self.entries\n    print(f'processing entry: {name}')\n    entry = self.entries[name]\n    file_type = entry['type']\n    data = self.get_entry_data(name)\n    filepath = outdir + '/' + entry['name'] + '.' + entry['type']\n    if '/' in filepath:\n      makedirs(path.dirname(filepath), exist_ok=True)\n    with open(filepath, 'wb') as outfile:\n      if gen_header and file_type in ['pdi','pdt','pdv','pda','pds','pft']:\n        hdr = self.construct_entry_header(name)\n        outfile.write(hdr)\n      outfile.write(data)\n\n  def save_entries(self, outdir, gen_header):\n    for name in self.entries:\n      self.save_entry_data(name, outdir, gen_header)\n\n  def print_entries(self):\n    for name in self.entries:\n      print(f'{name}: {self.entries[name][\"type\"]}')\n\nif __name__ == \"__main__\":\n  parser = ArgumentParser(prog=\"pdz.py\", description=\"Extract contents of a pdz file.\")\n  parser.add_argument(\"-o\", \"--outdir\", default=\"pdz_output\", help=\"output directory\", dest=\"out_dir\")\n  parser.add_argument(\"-i\", \"--infile\", help=\"input file\", dest=\"in_file\", required=True)\n  parser.add_argument(\"-l\", \"--list-files\", help=\"print a list of all entries in the file, ignoring all other arguments\",\n                      dest=\"list_files\", required=False, action=\"store_true\")\n  parser.add_argument(\"-g\", \"--gen-headers\", help=\"generate file headers for pd* files (experimental, default=false)\" , \n                      dest=\"gen_headers\", required=False, action=\"store_true\")\n  parser.add_argument(\"-f\", \"--extract-file\", help=\"extract the given file(s), or all if this arg isn't provided\",\n                      dest=\"file_list\", required=False, action=\"append\")\n  args = parser.parse_args()\n\n  pdz = PlaydatePdz.open(args.in_file)\n  \n  if args.list_files:\n    pdz.print_entries()\n    exit()\n  \n  if args.file_list:\n    for f in args.file_list:\n      pdz.save_entry_data(f, args.out_dir, args.gen_headers)\n  else:\n    pdz.save_entries(args.out_dir, args.gen_headers)\n"
  },
  {
    "path": "tools/usbeval.py",
    "content": "if (len(argv) < 2):\n  print('usbeval.py')\n  print('Evaluates a Lua script on a Playdate device, via USB')\n  print('Requires pdc from the Playdate SDK as well as the pyusb library')\n  print('Usage:')\n  print('python3 usbeval.py ./input.lua')\n  exit()\n\nimport tempfile\nimport subprocess\nimport usb.core\nimport usb.util\nfrom sys import argv\nfrom pathlib import Path\nfrom struct import unpack\nfrom zlib import decompress\nfrom time import sleep\n\n# Playdate USB vendor and product IDs\nPLAYDATE_VID = 0x1331;\nPLAYDATE_PID = 0x5740;\nIN_SIZE = 64;\n\ndef pdz_extract_entry(data, entry):\n  ptr = 0x10\n  while ptr < len(data):\n    flags = data[ptr]\n    is_compressed = (flags >> 7) & 0x1\n    innerlen = data[ptr + 1] | (data[ptr + 2] << 8)\n    filename = data[ptr + 4 : data.find(b\"\\0\", ptr + 4)]\n    outerheadersize = 4 + len(filename) + 1\n    outerheadersize = ((ptr + outerheadersize + 3) & ~3) - ptr\n    zlibdata = data[ptr + outerheadersize + 4: ptr + outerheadersize + innerlen]\n    if filename.decode('utf-8') == entry:\n      return decompress(zlibdata)\n    ptr += outerheadersize + innerlen\n  return None\n\ndef usb_connect():\n  # find our playdate device\n  device = usb.core.find(idVendor=PLAYDATE_VID, idProduct=PLAYDATE_PID)\n  if device is None:\n    raise ValueError('Device not found')\n\n  # set the active configuration. With no arguments, the first\n  # configuration will be the active one\n  device.set_configuration()\n\n  # get an endpoint instance\n  cfg = device.get_active_configuration()\n  intf = cfg[(1,0)]\n\n  epOut = usb.util.find_descriptor(\n      intf,\n      # match the first OUT endpoint\n      custom_match = \\\n      lambda e: \\\n          usb.util.endpoint_direction(e.bEndpointAddress) == \\\n          usb.util.ENDPOINT_OUT)\n\n  epIn = usb.util.find_descriptor(\n      intf,\n      # match the first IN endpoint\n      custom_match = \\\n      lambda e: \\\n          usb.util.endpoint_direction(e.bEndpointAddress) == \\\n          usb.util.ENDPOINT_IN)\n\n  assert epOut is not None\n  assert epIn is not None\n  device.reset()\n  return epOut, epIn\n\ndef usb_read_bytes(endPoint):\n  res = bytearray()\n  has_started = False\n  while True:\n    try:\n      b = bytearray(epIn.read(IN_SIZE))\n      res += b\n      if b != b'': has_started = True\n      if has_started and b == b'': break\n    except usb.core.USBTimeoutError:\n     break\n  return res\n\nwith tempfile.NamedTemporaryFile(prefix='main', suffix='.lua') as luafile, tempfile.TemporaryDirectory(suffix='.pdx') as pdxdir:\n\n  # copy lua file\n  print('reading input file')\n  with open(argv[1], 'rb') as infile:\n    luafile.write(infile.read())\n    luafile.seek(0)\n\n  # compile lua with pdc\n  print('compiling lua with pdc')\n  subprocess.run(['pdc', luafile.name, pdxdir])\n  luastem = Path(luafile.name).stem\n\n  # extract lua bytecode from pdz\n  print('extracting lua bytecode')\n  with open(Path(pdxdir, luastem + '.pdz'), 'rb') as pdzfile:\n    pdz = pdzfile.read()\n    bytecode = pdz_extract_entry(pdz, luastem)\n\n  # connect to playdate over usb \n  print('finding playdate connected to usb...')\n  epOut, epIn = usb_connect()\n  print('successfully connected to playdate!')\n\n  # set usb echo mode to off\n  print('setting usb echo to off')\n  epOut.write('echo off\\n')\n  resp = usb_read_bytes(epIn)\n\n  # get version info (to test things work, but also looks cool :^))\n  # print('playdate version info:')\n  # epOut.write('version\\n')\n  # resp = usb_read_bytes(epIn)\n  # print(resp.decode(\"utf-8\").strip())\n\n  # consume printed console content until there's nothing new\n  print('clearing current console data')\n  epOut.write('eval\\n')\n  sleep(.2)\n  usb_read_bytes(epIn)\n  \n  # send lua bytecode to the device\n  print('sending payload for device to eval...')\n  header = b'eval %d\\n' % len(bytecode)\n  payload = header + bytecode\n  epOut.write(payload)\n  sleep(.2) # payload seems to take a bit to execute, you may need to adjust this if you have a big payload\n  resp = usb_read_bytes(epIn)\n  print('===============')\n  print('console output:')\n  print(resp.decode(\"utf-8\").strip())\n\n  # keep polling for new console output\n  while True:\n    try:\n      sleep(.1)\n      resp = usb_read_bytes(epIn)\n      text = resp.decode(\"utf-8\").strip()\n      if text: print(text)\n    except KeyboardInterrupt:\n      break"
  },
  {
    "path": "usb/stream.md",
    "content": "# The `stream` protocol\n\nWhen `stream enable` is sent over serial, the Playdate enters streaming mode, used by Playdate Mirror.\nIn 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.\n\n(As with all Playdate formats, all numbers reported using the protocol are little-endian.)\n\n### Entering, exiting, and maintaining streaming mode\nIf the Playdate has been in streaming mode for over one second without a `stream poke` command sent over USB, it will stop reporting data.\nHowever, it won't completely exit streaming mode -- any future `stream enable` will *not* resend the entire screen afterward.\n\nTo exit streaming mode, send `stream disable` via serial.\n\n### Stream messages\nWhile the Playdate is streaming, it will report data in short messages.\nThe general format of one of these messages is:\n\n| **Offset** | **Data type** | **Content** |\n|:-----|:-----|:-----|\n| 0 | `uint16` | Message type (see below) |\n| 2 | `uint16` | Payload length in bytes |\n\nThe actual payload data follows this 4-byte header.\n\n### Audio control\nTo control how audio is reported, there are three options you can send during streaming mode.\n(The format the samples are parsed with isn't actually changed until the format switch is acknowledged with a stream message!)\n\n| **Command** | **Meaning** |\n|:-----|:-----|\n| `stream a+` | Switch to stereo signed 16-bit PCM audio |\n| `stream am` | Switch to mono signed 16-bit PCM audio |\n| `stream a-` | Don't send any audio (the default) |\n\n## Message types\n\n### `0x0001`: Input state\nReports the state of the Playdate's buttons and crank every frame.\n\n| **Payload offset** | **Data type** | **Content** |\n|:-----|:-----|:-----|\n| 0 | `uint16` | Button flags (see below) |\n| 2 | `uint16` | Unknown: Seems to change erratically |\n| 4 | `float` | Crank angle in degrees |\n\n#### Button flags\n| **Bitmask** | **Meaning** |\n|:-----|:-----|\n| `flags & 0x0001` | If `> 0`, d-pad left button is pressed |\n| `flags & 0x0002` | If `> 0`, d-pad right button is pressed |\n| `flags & 0x0004` | If `> 0`, d-pad up button is pressed |\n| `flags & 0x0008` | If `> 0`, d-pad down button is pressed |\n| `flags & 0x0010` | If `> 0`, B button is pressed |\n| `flags & 0x0020` | If `> 0`, A button is pressed |\n| `flags & 0x0040` | If `> 0`, Menu button is pressed |\n\n### `0x000A`: New frame (no delay)\nStarts a new frame as fast as possible, with no delay from the previous frame.\n\n### `0x000B`: End frame\nEnds the current frame. In the official Mirror app, this flips the framebuffer to make it visible on-screen.\n\n### `0x000C`: Update screen line\nSignals that a single horizontal line of the display was updated.\n\nOnly the lines that have changed since the last frame are sent, unless the current frame is the first one.\nIn that case, the entire screen is sent as 240 line update messages.\n\n| **Payload offset** | **Data type** | **Content** |\n|:-----|:-----|:-----|\n| 0 | `uint8` | Line number that was updated (starting at one and bit-reversed, so line 0 becomes `0b10000000 == 0x80`) |\n| 1 | `50 bytes` | Line data, left to right, with the least significant bit coming first in each byte |\n| 51 | `uint8` | Zero byte to pad the length to a multiple of 4 bytes |\n\n### `0x000D`: New frame (with delay)\nStarts a new frame a certain amount of time since the previous frame started.\n\n| **Payload offset** | **Data type** | **Content** |\n|:-----|:-----|:-----|\n| 0 | `uint32` | Milliseconds since the start of the previous frame |\n\n### `0x0014`: Audio frames (multiple)\nSends one or multiple audio frames to buffer for playback.\n\nThe data format is signed 16-bit PCM, with the left channel of each sample coming first.\n(If the audio format is mono, then the right channel will be zero.)\n\n### `0x0015`: Audio format switch acknowledge\nSignals that a requested change in audio formats has taken place.\n\n| **Payload offset** | **Data type** | **Content** |\n|:-----|:-----|:-----|\n| 0 | `uint16` | Audio format flags (see below) |\n\n#### Audio format flags\n| **Bitmask** | **Meaning** |\n|:-----|:-----|\n| `flags & 0x0001` | If `> 0`, audio is enabled |\n| `flags & 0x0002` | If `> 0`, audio has two channels |\n\n### `0x0016`: Audio frames (fill single)\nSends one audio sample to continuously play until more data is received. This usually denotes silence.\n\nLike with type `0x0014`, the data format is signed 16-bit PCM, with the left channel of the sample coming first.\n"
  },
  {
    "path": "usb/usb.md",
    "content": "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.\n\nSome 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.\n\n## USB details\n\n\n|||\n|:----------------|:------|\n| *USB Vendor ID* | `0x1331` |\n| *USB Product ID* | `0x5740` |\n| *Baud rate* | `115200` |\n\n## Connecting to the USB\n\nYou can use any terminal emulator to connect to the virtual serial port. For example, on macOS, you can connect using picocom by running:\n\n`picocom -b 115200 -p n -d 8 --omap crcrlf /dev/cu.usbmodemPD<serial number>`\n\nWhen you're done, disconnect and quit picocom with `CTRL-a` `CTRL-x`\n\n## USB commands\n\nRunning `help` will return a very helpful list of available commands:\n\n```\nThe following commands are available:\n\nTelnet commands:\n help        Displays all available commands or individual help on each command\n\nCPU Control:\n serialread  Print the device serial number\n trace       trace_<delay>. (trace 10)\n stoptrace   stoptrace\n bootdisk    reboot into recovery segment USB disk\n datadisk    reboot into data segment USB disk\n factoryreset factory reset\n formatdata  format data disk\n settime     sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2\n gettime     reads the RTC\n vbat        get battery voltage\n rawvbat     get raw battery adc value\n batpct      get battery percentage\n temp        get estimated ambient temperature\n dcache      dcache <on/off>: turn dcache on or off\n icache      icache <on/off>: turn icache on or off\n\nRuntime control:\n echo        echo (on|off): turn console echo on or off\n buttons     Test buttons & crank\n tunebuttons tunebuttons <debounce> <holdoff>\n btn         btn <btn>: simulate a button press. +a/-a/a for down/up/both\n changecrank changecrank +-<degrees>\n dockcrank   simulates crank docking\n enablecrank Reenables crank updates\n disablecrank Disables crank updates\n accel       simulate accelerometer change\n screen      Dump framebuffer data (400x240 bits)\n bitmap      Send bitmap to screen (followed by 400x240 bits)\n controller  start or stop controller mode\n eval        execute a compiled Lua function\n run         run <path to pdx>: Run the named program\n luatrace    Get a Lua stack trace\n stats       Display runtime stats\n autolock    autolock <always|onBattery>\n version     Display build target and SDK version\n memstats    memstats\n hibernate   hibernate\n\nStream:\n stream      stream <enable|disable|poke>\n\nESP functions:\n espreset    reset the ESP chip\n espoff      turn off the ESP\n espbootlog  get the ESP startup log\n 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.\n espflash    espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.\n espbaud     espbaud <speed> [cts]\n esp         esp <cmd>: Forward a command to the ESP firmware, read until keypress\n\nFirmware Update:\n fwup        fwup [bundle_path]\n\n```\n\nSecret commands:\n\n```\nformatboot\nunlock\nislocked\n```\n\nMost 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.\n\n### `version`\n\nExample output:\n\n```\n~version:\ntarget=DVT1\nbuild=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649\nboot_build=c4abdb37253e-1.7.0-release.127473-buildbot\nSDK=1.7.0\npdxversion=10500\nserial#=<REDACTED>\ncc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]\n```\n\n### `stats`\n\nExample output:\n\n```\n~stats:\nframe count: 194503\nframe time: 0.000977\ngc time: 0.016602\ndisp time: 18\ncurrent time: 9855691\nmem alloced: 403288\nmem reserved: 460448\nmem total: 16645684\nkernel: 0.1%\nserial: 0.0%\ngame: 2.5%\nGC: 35.2%\nwifi: 0.0%\ntrace: 0.0%\naudio: 0.2%\n```\n\n### `buttons`\n\nBegins 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.\n\nEach new state will be written as a single line with the following structure:\n\n```\nbuttons:XX XX XX crank:X.X docked:X\n```\n\n`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:\n\n| Button | Bitmask |\n|:-------|:--------|\n| a      | `0x01`   |\n| b      | `0x02`   |\n| up     | `0x04`   |\n| down   | `0x08`   |\n| left   | `0x10`  |\n| right  | `0x20`  |\n| menu   | `0x40`  |\n| lock   | `0x80`  |\n\n`crank` gives the crank angle as a floating point number, measured in degrees, with `0` being the 12 o'clock position.\n\n`docked` will be `0` if the crank is not docked, or `1` if docked.\n\n### `screen`\n\nGets 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.\n\n### `bitmap`\n\nSends 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.\n\n### `run`\n\nLaunches a .pdx rom from the Playdate's data partition. The game path must begin with a forward slash, e.g `run /System/Crayons.pdx`.\n\n### `eval`\n\nEvaluates 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.\n\n*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.*\n\nThis command will not work if the currently loaded game is from the System directory on the device, presumably for security reasons.\n\n### `stream`\n\nUsed for interacting with the Playdate's [video/audio streaming protocol](/usb/stream.md), as used by Playdate Mirror.\n\n### `esp`\n\nUsing 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!\n\nAfter 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.\n\nI 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:\n\n[**`AT+GMR`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-gmr-check-version-information):\n\nVersion information:\n\n```\nAT version:2.0.0.0-dev(b6850a4 - Oct 24 2019 12:10:13)\nSDK version:v3.3-beta3-170-g91f29bef17\ncompile time(e9c8abb):Dec 15 2021 20:08:07\n```\n\n[**`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):\n\nQuerying supported commands doesn't seem to be supported.\n\n[**`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):\n\nCurrent UART configuration:\n\n```\n+UART_CUR:2534653,8,1,0,3\n```\n\n[**`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):\n\nQuerying user partitions in ESP flash:\n\n```\nAT+SYSFLASH?\n+SYSFLASH:\"ble_data\",64,1,0x21000,0x3000\n+SYSFLASH:\"server_cert\",64,2,0x24000,0x2000\n+SYSFLASH:\"server_key\",64,3,0x26000,0x2000\n+SYSFLASH:\"server_ca\",64,4,0x28000,0x2000\n+SYSFLASH:\"client_cert\",64,5,0x2a000,0x2000\n+SYSFLASH:\"client_key\",64,6,0x2c000,0x2000\n+SYSFLASH:\"client_ca\",64,7,0x2e000,0x2000\n+SYSFLASH:\"factory_param\",64,8,0x30000,0x1000\n+SYSFLASH:\"wpa2_cert\",64,9,0x31000,0x2000\n+SYSFLASH:\"wpa2_key\",64,10,0x33000,0x2000\n+SYSFLASH:\"wpa2_ca\",64,11,0x35000,0x2000\n+SYSFLASH:\"mqtt_cert\",64,12,0x37000,0x2000\n+SYSFLASH:\"mqtt_key\",64,13,0x39000,0x2000\n+SYSFLASH:\"mqtt_ca\",64,14,0x3b000,0x2000\n+SYSFLASH:\"fatfs\",1,129,0x70000,0x90000\n```\n\nMost 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`.\n\n[**`AT_FS`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#esp32-only-at-fs-filesystem-operations):\n\nNo file system commands seem to be supported.\n\n### `btn`\n\nThe `btn` command can also send key presses and releases, which call the associated callbacks for the current game.\n\nMost 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.\n\nYou can find a full set of the key codes used by the Simulator [here](https://gist.github.com/scratchminer/dcbf3410a7e72151ea68273adce9c932).\n\nFor a key press event, send `btn vK`, where `K` is the key code byte.\n\nFor a key release event, send `btn ^K`, again where `K` is the key code byte.\n\n### `unlock`\n\nTakes a 32 character unlock code and compares it to the device's unlock code.\n\nIf it matches, [additional commands](usb_unlocked.md) are enabled.\n\nThe unlock code is likely unique to each device and is located in different memory regions depending\non the playdate hardware revision.\n- As of firmware 1.10, HW revision A contains the key at address `0x1FF0F040`.\n- HW revision B contains the key at address `0x08FFF040`, located in the OTP region of the flash memory.\n\nThe following code snippet may be used to dump your key. Use the memory location that corresponds to\nthe HW revision you have. Even though the memory region is protected in unprivileged mode, in firmware\n2.4.2, the write succeeds and then the console crashes. You may then find the key in the data drive.\n\n```c\nSDFile* file = pd->file->open(\"unlockkey.txt\", kFileWrite);\npd->file->write(file, (const char*)0x1FF0F040, 0x20);\npd->file->close(file);\n```\n\n### `islocked`\n\nPrints `1` if the serial console is locked, or `0` if the device successfully ran `unlock`.\n\n## Changes\n\n### 2.0\n\n- Added `rawvbat` command\n\n### 1.12.3\n\n(these commands were observed in 1.12.3, but may have been introduced earlier)\n\n- Added `factoryreset` command\n- Added `tunebuttons` command\n- Changed `autolock` to remove never option.\n\n### 1.7.0\n\n- Added the `hibernate` command.\n- `version` output:\n  ```\n  ~version:\n  target=DVT1\n  build=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649\n  boot_build=c4abdb37253e-1.7.0-release.127473-buildbot\n  SDK=1.7.0\n  pdxversion=10500\n  serial#=<REDACTED>\n  cc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]\n  ```\n\n### 1.4.0\n\nInitial version tested\n"
  },
  {
    "path": "usb/usb_unlocked.md",
    "content": "## USB commands in unlocked mode\n\nThis is the output of running `help` on a Playdate with firmware 1.10 after running `unlock`:\n\n```\nThe following commands are available:\n\nTelnet commands:\n help        Displays all available commands or individual help on each command\n loop        loop next command x times with delay y ms (eg loop 10 100 help)\n lock        Lock the firmware command interface\n\neMMC kvstore:\n kvget       kvget <key> [len]\n kvput       kvput <key> <len>\n kvrm        kvrm <key>\n kvwipe      kvwipe\n\nCPU Control:\n gpio        gpio <port> <i(pu/pd)/1(pu/pd)/0(pu/pd)>. (gpio A3 1 or gpio B2 ipu)\n i2cread     i2cread <unit> <address> <register> <len>. (i2cread 0 0x47 0x38 3)\n i2cwrite    i2cwrite <unit> <address> <register> <value>. (i2cwrite 3 0x47 0x38 0xff)\n tlv493read  tlv493read <address> <len>. (tlv493read 0x47 3)\n tlv493write tlv493write <address> <val>. (tlv493write 0x47 0x00 0x37 0x48)\n serialwrite serialwrite <string>\n serialread  Print the device serial number\n pwwrite     pwwrite <string>\n pwread      pwread\n reset       reset system\n dfu         reset system\n trace       trace_<delay>. (trace 10)\n stoptrace   stoptrace\n bootdisk    reboot into recovery segment USB disk\n datadisk    reboot into data segment USB disk\n sysdisk     reboot into system segment USB disk\n factoryreset factory reset\n formatboot  format recovery disk\n formatdata  format data disk\n formatsys   format system disk\n shutdown    put device in stop mode\n cleartime   clear time from device\n settime     sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2\n gettime     reads the RTC\n rtccalib    rtccalib <on/off>: enable rtc calibration output\n vbat        get battery voltage\n batpct      get battery percentage\n temp        get estimated ambient temperature\n peek        peek <addr>: Read a 32-bit value from memory\n poke        poke <addr> <value>: Write a 32-bit value int memory\n dump        dump <start> <end>: Dump memory in range [start-end) as bytes\n dumpw       dumpw <start> <end>: Dump memory in range [start-end) as words\n stop        stop cpu\n led         led <R> <G> <B>. (led 0-255 0-255 0-255)\n leddemo     leddemo\n charge      charge <on/off>: enables/disables battery charging\n readcharger readcharger\n burn        waste cpu to run down the battery\n lsedrive    get LSE drive level\n extv        get 5V_ext voltage\n dcache      dcache <on/off>: turn dcache on or off\n icache      icache <on/off>: turn icache on or off\n rcccsr      report bootinfo.rcccsr value\n rdp         rdp <enable/disable>\n suspendusb  sets USB current limit to 500uA\n\neMMC Control:\n emmctest    emmctest\n emmcinfo    emmcinfo\n emmcwipe    emmcwipe\n emmcdump    emmcdump <addr> <len>\n\nAudio Control:\n audiotest   Send test data to audio output\n audiosweep  audiosweep <startfreq> <endfreq> <length>: Send sweep signal to audio output. Frequencies are (integer) Hz, length is milliseconds\n stopaudio   Stop audio output\n startaudio  Start audio output\n audioout    audioout <speaker|headphone|both|none|bt|onboard>\n mictest     mictest <int|ext> <filename> <length>: Record microphone to given file. length is in milliseconds\n blowtest    blowtest\n micbiastest micbiastest <level:0-5> <verbose:0/1>\n\nEncryption:\n encrypt     encrypt <file_in> <file_out>\n decrypt     decrypt <file_in> <file_out>\n\nRuntime control:\n echo        echo (on|off): turn console echo on or off\n buttons     Test buttons & crank\n tunebuttons tunebuttons <debounce> <holdoff>\n btn         btn <btn>: simulate a button press. +a/-a/a for down/up/both\n changecrank changecrank +-<degrees>\n dockcrank   simulates crank docking\n enablecrank Reenables crank updates\n disablecrank Disables crank updates\n accel       simulate accelerometer change\n pause       Pause execution\n resume      Resume execution\n restart     Restart Lua runtime\n step        Step one frame\n fps         fps to return current frame rate, fps <frame rate> to set\n screen      Dump framebuffer data (400x240 bits)\n bitmap      Send bitmap to screen (followed by 400x240 bits)\n lcdtest     Draw a marching stripes pattern on the screen\n controller  start or stop controller mode\n eval        execute a compiled Lua function\n run         run <path to pdx>: Run the named program\n whatsrunning Returns the path of the currently running program\n luatrace    Get a Lua stack trace\n stats       Display runtime stats\n autolock    autolock <always|onBattery>\n version     Display build target and SDK version\n station     station <travel agent station name>: Configure device for running at the named station\n setvolume   setvolume <amt>:, 0-255\n getvolume   Returns the volume level 0-255\n wifi        wifi <GET|POST> <url> [file1] [file2]\n wifitest    wifitest <network> <password>\n memtest     memtest\n memstats    memstats\n woprset     woprset [server]\n woprget     woprget\n syncperiod  syncperiod <initial_s> [period_s]\n hibernate   hibernate\n\nFilesystem Stuff:\n listfiles   listfiles path: list files at path\n getfile     getfile <path>: get file contents at path\n putfile     putfile <path> <size>: upload file to path\n mkdir       mkdir <path>\n delete      delete <path>\n rmdir       rmdir <path>: Recursively delete directory\n unzip       unzip zip_path out_path\n md5         md5 <file_path>\n\nmemio commands:\n memiomd5    memiomd5 <addr> <size>\n\nStream:\n stream      stream <enable|disable|poke>\n\nESP functions:\n espreset    reset the ESP chip\n espoff      turn off the ESP\n espbootlog  get the ESP startup log\n 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.\n espflash    espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.\n espbaud     espbaud <speed> [cts]\n esp         esp <cmd>: Forward a command to the ESP firmware, read until keypress\n esptest     esptest <baud> <cts>\n espwipe     espwipe\n espversion  espversion\n wifiperf    wifiperf <addr> <port>\n wifistop    wifistop\n\nFirmware Update:\n fw          fw\n fwup        fwup [bundle_path]\n recovery    recovery\n unstage     unstage\n\nunzip test:\n unzipmem    unzipmem <addr> <len> <path>\n\nMemfault Tests:\n hardfault   Trigger a hardfault\n assert      Trigger an assert\n mflt        mflt <status|clear|send>\n\n```\n\n## Changes\n\n### 1.12.x\n\n- Added `factoryreset`\n- Added `tunebuttons`\n- Added `never` option for `autolock`\n"
  }
]