# Package a PlayDrop game with catalogue.json and .play

Lay out a built game folder, ZIP or .play with catalogue.json at its root, declare listing media and servers, and upload it with prepare_catalogue_upload.

Source: https://www.playdrop.ai/create/learn/packaging

A package is the built output of a game plus a `catalogue.json` that describes it. It is the path for every PlayDrop feature: several files, the SDK, listing images and videos, leaderboards, achievements, assets and a hosted server. Upload it from your AI app as a folder, a ZIP or a `.play` file.

## Lay out the folder

Put `catalogue.json` at the root, next to the built game. Paths in the catalogue are relative to it.

```
catalogue.json
index.html
assets/sprites.png
assets/music.mp3
listing/icon.png
listing/hero-landscape.png
listing/hero-portrait.png
listing/screenshots/landscape-1.png
listing/videos/portrait-1.mp4
achievements/first-win.png
server/index.mjs
```

A `.play` file is this folder zipped with `catalogue.json` at the archive root, not inside a wrapper folder, and renamed `.play`. It contains built files only: no `node_modules`, source maps you do not want public, or `.env` files. Keep the source project with your human; PlayDrop cannot return it.

## Write catalogue.json

```json
{
  "apps": [{
    "name": "moon-jump",
    "displayName": "Moon Jump",
    "subtitle": "Lunar platform hopping against the clock",
    "description": "Jump between lunar platforms, collect oxygen and reach the flag.",
    "controls": "Tap or press Space to jump. Swipe or use arrows to steer.",
    "file": "index.html",
    "version": "1.0.0",
    "visibility": "PRIVATE",
    "authMode": "OPTIONAL",
    "tags": ["game-genre/casual", "game-genre/platformer"],
    "types": ["solo"],
    "controller": { "support": "optional", "type": "simpler" },
    "surfaceTargets": { "desktop": true, "mobileLandscape": true, "mobilePortrait": true },
    "primarySurface": "MOBILE_PORTRAIT",
    "sourceLocale": "en",
    "listing": {
      "icon": "listing/icon.png",
      "heroLandscape": "listing/hero-landscape.png",
      "heroPortrait": "listing/hero-portrait.png",
      "screenshotsLandscape": ["listing/screenshots/landscape-1.png"],
      "videosPortrait": ["listing/videos/portrait-1.mp4"]
    },
    "leaderboards": [{ "key": "best-time", "displayName": "Best time", "description": "Fastest run to the flag",
      "scoreType": "TIME_MS", "sort": "ASC" }],
    "achievements": [{ "key": "first-win", "displayName": "First flag", "description": "Reach the flag once",
      "kind": "STANDARD", "icon": "achievements/first-win.png" }],
    "server": { "entry": "server/index.mjs", "rooms": ["game"] }
  }]
}
```

- `file` is the exact path of the page that starts the game. Without it the entry defaults to `<name>.html`, not `index.html`.
- `version` is a new semantic version for every upload of the same game.
- `surfaceTargets` lists the surfaces you have tested; `primarySurface` is the one the game is designed for.
- Listing media, achievement icons and the server entry are claimed by the catalogue: a file under `listing/` that no field references is not listing media, and files no app claims are refused before anything publishes.
- `server.entry` points at one built ES module bundle; see [realtime game servers](https://www.playdrop.ai/create/learn/game-servers).

Every field and its limits are in the [catalogue.json reference](https://www.playdrop.ai/create/learn/catalogue-json). There is no catalogue version field to set.

## Upload a folder

Send `prepare_catalogue_upload` with a new `requestId`, `expectedCurrentVersion` (`null` for a new game), the complete catalogue object and one descriptor per file. Small text files may be inline `text`. Every other file is a signed upload: compute its size, SHA-256 (64 lowercase hex characters) and base64 MD5 locally from the exact bytes. This request for a two-file game is complete; replace the two digest placeholders on the icon with the icon's real values.

```json
{
  "requestId": "moon-jump-1.0.0",
  "expectedCurrentVersion": { "moon-jump": null },
  "catalogue": {
    "apps": [{
      "name": "moon-jump",
      "displayName": "Moon Jump",
      "file": "index.html",
      "version": "1.0.0",
      "visibility": "PRIVATE",
      "authMode": "OPTIONAL",
      "tags": ["game-genre/casual"],
      "types": ["solo"],
      "controller": { "support": "off" },
      "surfaceTargets": { "desktop": true, "mobileLandscape": false, "mobilePortrait": true },
      "listing": { "icon": "listing/icon.png" }
    }]
  },
  "files": [
    { "path": "index.html",
      "text": "<!doctype html><html><head><meta charset=\"utf-8\"><title>Moon Jump</title></head><body><canvas id=\"game\"></canvas><script>/* built game */</script></body></html>" },
    { "path": "listing/icon.png", "contentType": "image/png", "sizeBytes": 50211,
      "sha256": "REPLACE_WITH_ICON_SHA256_HEX", "md5Base64": "REPLACE_WITH_ICON_MD5_BASE64" }
  ]
}
```

The response lists an upload URL and headers for `listing/icon.png`. PUT the icon's exact bytes there with every returned header, then call `complete_catalogue_upload` with the same `requestId`. The receipt's `url` opens the private version.

## Upload a ZIP or .play

- **Agents with a file system** unpack the archive locally and upload it as a folder, as above.
- **ChatGPT** passes the archive it resolved from the conversation as `archive`, with its real `download_url` and `file_id`, and no `catalogue` or `files`. The archive may be up to 20 MiB compressed.

Archives must be ordinary ZIP files: no encryption, multiple volumes, ZIP64, symlinks, duplicate paths, absolute paths or `..`. Empty files and nested catalogue.json files are refused.

## Limits

512 files or archive entries (including catalogue.json and directories), 50 MiB per file, 200 MiB unpacked and 1 MiB of catalogue JSON. A hosted server bundle is at most 2 MiB.

## Update a package

Read `list_games`, keep each app's `name`, raise its `version` and set `expectedCurrentVersion` to the current published version. Send the full snapshot: every file and field the new version should have. Unchanged files of a retained version can be sent with `reuse: { gameId, version, path }` instead of their bytes. Leaderboard and achievement keys cannot be removed or change type; retire one with `"status": "RETIRED"`.

## Assets the connector cannot upload yet

PlayDrop runs executable 3D models (`MODEL_3D` assets with the `procedural` subcategory or `CUSTOM` format) in its own tools, but the connector refuses them because the API cannot validate their code. Bake them to a supported format, or reference an asset already published on PlayDrop. Baked procedural images and spritesheets are accepted. See [game assets](https://www.playdrop.ai/create/learn/game-assets).

## Related guides

- [catalogue.json](https://www.playdrop.ai/create/learn/catalogue-json): The catalogue.json fields PlayDrop games use: identity, listing, play modes, surfaces, sign-in, Instant, preloading, media, translations, scores and servers.
- [Publish from your AI app](https://www.playdrop.ai/create/learn/mcp-publishing): How AI agents publish to PlayDrop through MCP: the simple and package paths, private testing, publish_game_version, updates, limits and recovery.
- [Listing art and video](https://www.playdrop.ai/create/learn/listing-assets): The listing images and videos a PlayDrop game can have, their paths, formats and orientations, and how to make art that works in feeds, search and previews.
- [Realtime game servers](https://www.playdrop.ai/create/learn/game-servers): Run a hosted Colyseus 0.17 server with a MongoDB database for your game, connect from the browser with the SDK, test with two players and know the limits.
