# Game manifest

A game describes itself in a `unihub.manifest.json` at the root of its folder, and the
Hub reads nothing else about it: change the manifest and submit a new version to update its
listing. `package.json` stays only to build the game. The JSON Schema
[`unihub.manifest.v1.json`](https://play.unikia.es/schema/unihub.manifest.v1.json) holds every
rule below; point `$schema` at it and editors autocomplete and explain each field.

## `unihub.manifest.json`

```json
{
  "$schema": "https://play.unikia.es/schema/unihub.manifest.v1.json",
  "manifestVersion": 1,
  "id": "heated-heist",
  "title": "Heated Heist",
  "version": "0.1.0",
  "developers": ["cebun"],
  "description": "Rob fictional estates while every sound brings more armed security.",
  "tagline": "Get in. Get rich. Get out.",
  "details": "Steal jewellery and money from rich people's fictional mansions...",
  "genre": "Shooter",
  "tags": ["Top-down shooter", "Heist", "Stealth", "Co-op", "Online co-op", "Arcade", "2D"],
  "players": { "min": 1, "max": 2 },
  "platforms": ["desktop", "mobile"],
  "languages": ["en", "es"],
  "color": "#f382cd",
  "cover": "public/assets/menu-bg.webp",
  "squareCover": "public/assets/square.webp",
  "badge": "public/assets/badge.webp",
  "path": "heist-game",
  "achievements": "public/achievements.json",
  "changelog": "CHANGELOG.md",
  "sdk": "^1.4.0"
}
```

| Field | Required | Meaning |
|---|---|---|
| `manifestVersion` | yes | Always `1`. |
| `id` | yes | The stable game id ([game id](achievements.md#game-id)): `a-z`, `0-9` and `-`, at most 40 characters. Never change it once published. |
| `title` | yes | The game's name in the Hub, at most 40 characters. |
| `version` | yes | Semver, shown in Game details. It wins over `package.json`'s; `uniplay validate` warns when they differ. |
| `developers` | yes | 1 to 10 Hub nicknames of the people who make the game, shown as its Developers in Game details. Only they can [submit](submitting.md) it. |
| `description` | yes | One-line pitch shown under the title, at most 160 characters. |
| `tagline` | yes | A short slogan on the artwork, at most 60 characters; the Hub shows it in capitals. |
| `details` | no | Longer text for Game details, at most 600 characters. Defaults to `description`. |
| `genre` | yes | The one main genre, at most 24 characters, e.g. "Shooter", "Roguelite". Everything else goes in `tags`. |
| `tags` | no | Up to 12 more descriptors, each at most 24 characters, most important first: sub-genres, modes, setting, style (e.g. "Top-down shooter", "Co-op", "Pixel art", "2D"). Shown as written. |
| `players` | yes | How many can play. A number is exactly that many: `1` for single player, `2` for a game that always needs two. A range `{ "min": 1, "max": 4 }` is a game playable solo or with up to four (co-op or versus). Shown as "1 player", "2 players" or "1–4 players". |
| `platforms` | no | Devices the game is made for: one or more of `"desktop"` (keyboard, mouse or controller) and `"mobile"` (touch screens). Shown as icons in the Hub. Defaults to `["desktop"]`; list `"mobile"` only when the game is playable with touch alone. |
| `languages` | no | Languages the game can be played in, as [BCP 47](https://www.rfc-editor.org/info/bcp47) codes such as `"en"`, `"es"` or `"pt-BR"`, the original language first. Game details lists them by name. Defaults to `["en"]`. |
| `playthroughs` | no | Whether Game details offers **Start a new game**, which puts the player's saves aside as another playthrough they can switch back to. Set it to `false` for a game that keeps its own save slots (characters, save files), so players start over inside the game instead. Playthroughs already put aside stay listed. Defaults to `true`. |
| `fullscreen` | no | Whether the game asks to be played fullscreen, such as a first-person shooter. When it starts in Uniplay for desktop, the window goes fullscreen, unless the player turned that off in **Settings → General**, and comes back when they leave the game. On the site the game stays in the page. Defaults to `false`. |
| `coopAcrossVersions` | no | Whether buds on different versions of the game can play together. By default they can't: the game sees only the peers on its version, and the Hub offers the one behind an update before they join. Set it to `true` when versions stay compatible. Defaults to `false`. |
| `color` | no | Accent colour, `#rrggbb`. |
| `cover` | one of the two | Path to 16:9 key art, `.webp`, `.png` or `.jpg` (1600×900 `.webp` works best), for the game's page and the list view. The Hub shows it as is, without drawing the title over it, so include the logo in the art. |
| `squareCover` | one of the two | Path to 1:1 key art, `.webp`, `.png` or `.jpg` (1024×1024 `.webp` works best), for the grid view, with the logo in it too. A game needs `cover`, `squareCover` or both: without a square cover the grid crops the middle of the 16:9 one, and without a 16:9 cover the Hub fits the square one over a blurred copy of itself. |
| `badge` | no | Path to the game's [platinum](achievements.md#platinum) badge, `.webp`, `.png` or `.jpg`, 1:1 (a 256×256 `.webp` with a transparent background works best). Players see it greyed out on their profile and in Game details until they unlock every achievement. Without one, the Hub cuts a round badge from the cover. |
| `entry` | no | The built game's HTML file. Its folder is served at `/<path>/`. Defaults to `dist/index.html`. |
| `achievements` | no | Path to the game's [`achievements.json`](achievements.md#definitions-achievementsjson). |
| `changelog` | no | Path to the game's release notes, in the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. Translations sit next to it with a language tag before the extension (`CHANGELOG.es.md`, `CHANGELOG.pt-BR.md`); players see the one in their Hub language (or its language without the region), else this one. |
| `sdk` | no | The SDK range the game was built against, e.g. `^1.4.0`. |
| `path` | no | URL folder the game's page is at, `/<path>/`. Defaults to the id. The game itself plays from its version's own address, `/g/<id>/<version>/`, so load its files by relative URLs (`./assets/…`), never `/<path>/…`. `admin`, `api`, `assets`, `developers`, `g`, `healthz`, `library`, `licenses`, `policy`, `publisher`, `schema` and `uniplay` are reserved, and the Hub may assign another one on review to avoid clashes. |

Every path is relative to the manifest and stays inside the game's folder. The order
of the game list and which games the Store features are the Hub's choice.

## The game package

A game reaches the Hub as one zip: the game's own folder once built, with only what the
manifest names. `npx -p @unikia/upe-cli uniplay pack` writes it for you.

```text
heated-heist.zip
├── unihub.manifest.json         required
├── package.json                 required
├── dist/                        required: the built game, entry at dist/index.html
├── public/assets/menu-bg.webp   the cover, at the path the manifest names
├── public/assets/square.webp    the square cover, likewise, if there is one
├── public/assets/badge.webp     the platinum badge, likewise, if there is one
├── public/achievements.json     optional, see achievements
└── CHANGELOG.md                 optional release notes (Keep a Changelog), CHANGELOG.es.md…
```

- `unihub.manifest.json` and `package.json` are at the root of the zip, in no folder.
- The built game is under the entry's folder (`dist/` by default), and the covers, badge,
  achievements and changelog (with its translations) at the paths the manifest names.
- At most 500 MB zipped, 1 GB unzipped and 5,000 files.
- No symbolic links, absolute paths, `..` in paths, `__MACOSX/` or `.DS_Store`.
- The entry's folder must work from any base path: the Hub serves it from `/<path>/`, and
  may change `path` on review. Build it with relative URLs (Vite: `--base=./`).
- When published, the entry's folder is served under `/<path>/` with the entry renamed
  to `game.html`, the achievements next to it as `achievements.json`, the cover at
  `/uniplay/covers/<id>.<ext>`, the square cover at `/uniplay/covers/square/<id>.<ext>` and the
  badge at `/uniplay/badges/<id>.<ext>`.
  The changelog is parsed into
  `/uniplay/changelogs/<id>.json` (releases newest first, each with its `### Added`,
  `### Changed`… sections; `[Unreleased]` shows as the latest changes), which Game
  details opens from the version; translations into `<id>.<lang>.json` (keep `### Added`… in English). `/<path>/` itself serves the Hub page, which
  opens the game in a frame ([the Hub shell](hub.md#the-hub-shell)). `game.html` next to the
  entry is reserved.
- `uniplay validate [folder]` checks a game's folder once it's built: the manifest
  against its schema, `package.json`, that every file the manifest names exists, that the
  cover is a 16:9 image, the square cover and badge 1:1 ones, and that the achievements pass
  [their own schema](https://play.unikia.es/schema/achievements.v1.json). The Hub runs the
  same checks on every [submission](submitting.md).
