# The game manifest

Every game has a manifest. It says what the game is and declares anything a
host needs to know **before** the game runs: its score boards, achievements,
analytics events, input actions, audio channels and so on.

The manifest is written in the game's project, as the `manifest` in its hopple
config (see [the CLI](cli.md#the-projects-config)), and uploaded
beside the game's HTML file as `game.manifest.json`.

The host reads the manifest itself. The game never sends it over the bridge,
and the host never asks the game what it is allowed to do.

## A minimal manifest

```json
{
  "api": "1.0",
  "id": "ping",
  "version": "1.0.0",
  "title": "Ping",
  "orientation": "portrait",
  "audio": null
}
```

These six keys are required. Everything else is optional. A hopple config may
leave `api` and `previews` out: the CLI fills `api` from the installed
`@hopple/sdk`, and `previews` from the preview packages the project installs.

| Key | Rules | Meaning |
|---|---|---|
| `api` | `MAJOR.MINOR`, or `next` | The API the game is written against: the major picks what the game is served, the minor is the least it needs. `next` is the pre-releases of the next stable release. Left out of a config, the CLI takes it from the installed `@hopple/sdk` (`1.6.2` is `1.6`, a pre-release is `next`), and refuses one that differs. Publishing is refused until the major has a release of that minor, on `next` until it has a release, and on `next` for users who are neither trusted, admins, nor explicitly granted access. |
| `id` | lowercase letters, digits and single hyphens; at most 64 characters | The game's name. On the server, a game is `<owner>/<id>`. |
| `version` | `MAJOR.MINOR.PATCH`, optional `-prerelease` | The game's own version. Shown to the game as `sdk.info.game.version`. |
| `title` | at most 60 characters | What players see. |
| `description` | optional; at most 500 characters | A sentence or two shown beside the title. |
| `genres` | optional; see [`genres`](#genres) | What kind of game it is, the main one first. |
| `orientation` | `portrait`, `landscape` or `any` | The shape the game is designed for. Hosts lay the game out to match. |
| `audio` | `null`, or an object | Whether the game makes sound, and its volume channels. See [`audio`](#audio). |

## Validating

`hopple check` validates the manifest and names every problem with its field,
with the validator of the installed `@hopple/sdk`, which is of the API the
game declares. The CLI, the dev shell, the server and the clients all use the
same function, so they agree on whether a manifest is valid.

- **Unknown keys are refused.** A misspelt key (such as `"orientaton"`) is an
  error, not silently ignored. A host reading a published manifest ignores
  keys it doesn't know (`validateManifest(value, { lenient: true })`), since
  the game may be written against a newer minor than the host.
- **Known keys are checked strictly.** A misspelt value (such as
  `"draw_under"`) is an error, not silently ignored.

### In an editor

The `hopple` CLI ships the hopple config's JSON Schema, the manifest's
included, as `config.schema.json`, for completion, hover docs and errors as
you type. A JSON config in a project with `hopple` installed can point at it:

```json
{ "$schema": "./node_modules/hopple/config.schema.json", "page": "dist/index.html", "manifest": { ... } }
```

A TypeScript config gets the same checks from its type,
`import type { HoppleConfig } from 'hopple'`, whose manifest is the type of
the installed `@hopple/sdk`.

A few rules relate one part of a manifest to another, which JSON Schema can't
express: buttons must name declared actions, a control's `as` must name its
own buttons, and tiers must be in order and short of the target. An editor
won't flag them, but `hopple check` will.

**Tip:** write the config in TypeScript, as `hopple create` sets up. Then a
mistake in the manifest is a type error, and your code and manifest can share
constants.

## Optional keys

Each SDK feature that needs the host to know something in advance has its own
top-level key. The sections below show each one; see
[the SDK's guide](sdk.md) for how to use the feature from code.

### layout

```json
"layout": { "insets": "draw-under" }
```

| `insets` | Meaning |
|---|---|
| `reserve` (default) | The host shrinks the game's frame so none of the host's own buttons, or the device's notch, cover it. |
| `draw-under` | The frame fills the whole screen. The game reads `sdk.layout` and keeps its own important content clear of what covers it. |

This has to be in the manifest because the host sizes the frame before the
game has started.

### analytics

```json
"analytics": {
  "events": {
    "level.complete": { "level": "number", "seconds": "number" },
    "shop.open": {}
  }
}
```

Declares every event name the game may send, and the type of each property:
`string`, `number` or `boolean`.

- An event whose name isn't declared is refused (and the game is told).
- A property of the wrong type is dropped; the event is kept.
- Every property is optional.
- Names use lowercase letters, digits, `.`, `_` and `-`. Names starting with
  `hopple.` are reserved for the host's own records.
- Limits: 64 events, 8 properties per event, names up to 48 characters,
  property names up to 24.

### save

```json
"save": { "bytes": 2048 }
```

Says the game keeps a save and roughly how big it will be (at most 1 MiB).
Hosts that load saves from a server use this to know which games to prefetch.
A game without this key can still use `sdk.save`.

### score

```json
"score": {
  "boards": {
    "points": { "title": "High score", "direction": "desc" },
    "speed":  { "title": "Best time",  "direction": "asc", "format": "duration-ms" }
  }
}
```

| Field | Required | Meaning |
|---|---|---|
| `title` | yes | What players see. At most 40 characters. |
| `direction` | yes | `desc`: higher is better. `asc`: lower is better (times, strokes). |
| `format` | no | `integer` (default), `decimal`, or `duration-ms` (so `224310` shows as `3:44.31`). |
| `version` | no | 1 by default. Raising it starts the board again; see [Changing the manifest in a new version](#changing-the-manifest-in-a-new-version). |

`boards` is a map even for one board, so a game can add a second one later
without changing the meaning of the first. At most 16 boards.

### achievements

```json
"achievements": {
  "list": {
    "first-ping": { "title": "Hello there", "detail": "Press unlock, once", "icon": "👋" },
    "centurion": {
      "title": "Centurion",
      "progress": {
        "target": 100,
        "tiers": [{ "at": 10, "title": "Bronze" }, { "at": 50, "title": "Silver" }]
      }
    },
    "speedrun": {
      "title": "Under two minutes",
      "progress": { "target": 120000, "direction": "asc", "format": "duration-ms" }
    },
    "the-ending": { "title": "You found it", "secret": true }
  }
}
```

There are two kinds:

- **Flags** (no `progress`): earned once, by `sdk.achievements.unlock(id)`.
- **Measured** (with `progress`): the game reports a value and the achievement
  is earned when it reaches `target`. `direction` is `desc` (more is better,
  the default) or `asc` (less is better). `format` works as for score boards.
  `tiers` are named milestones on the way to the target; they are labels, not
  separate achievements.

`secret: true` hides the title and detail from players until it is earned.
The manifest is public, so this only stops spoilers.

`version` is 1 by default. Raising it starts the achievement again for
everybody; see [Changing the manifest in a new version](#changing-the-manifest-in-a-new-version).

Limits: 128 achievements, titles up to 40 characters, details up to 120, icons
up to 8 characters, 8 tiers.

### input

```json
"input": {
  "supported": [["touch"], ["gamepad"], ["keyboard", "mouse"]],
  "actions": {
    "shoot":   { "bind": ["key.KeyZ", "pad.south"], "title": "Shoot", "label": "A" },
    "special": { "bind": ["key.KeyC"], "title": "Special", "label": "C" },
    "pause":   { "bind": ["key.KeyP", "pad.start"], "title": "Pause" }
  },
  "controls": {
    "play": {
      "left":  { "move": { "type": "thumbstick", "as": "pad.left.stick" } },
      "right": { "acts": { "type": "buttons", "shape": "column", "buttons": ["shoot", "special"] } }
    }
  }
}
```

- **`supported`**: which input devices the game can be played with. Each entry
  is one set of devices that together are enough. The example means: touch
  alone, or a gamepad alone, or keyboard and mouse together. Devices are
  `keyboard`, `mouse`, `touch` and `gamepad`. This is the author's claim and
  is not checked against the bindings (a game may read input directly). A
  host can use it, through `playableWith`, to decide whether to offer a game
  to a player at all.
- **`actions`**: named buttons the game reacts to, and their default bindings.
  `title` is shown on the host's key-binding screen; `label` is printed on the
  on-screen button, and is what `sdk.input.mapping` calls the action on a touch
  screen. Both are optional; `bind` is not.
- **`controls`**: named layouts of on-screen controls for touch screens. Each
  layout places controls in zones (`left`, `right`, `full`). A control is
  either a `thumbstick` (with `as` naming which stick it acts as, and an
  optional `axis` of `x` or `y` holding it to one direction) or a set of
  `buttons` (naming actions) arranged as a `row`, `column`, `diamond` or
  `cluster`. An unknown shape is drawn as a column. A stick held to an axis is
  drawn as a track and always reads 0 on the other axis; a real pad's stick
  still reports both. Any control may have a `scale` from 0.75 to 1.5, its
  size next to the layout's others (1 when left out).

Binding names:

| Binding | Means |
|---|---|
| `key.KeyZ` | A physical key, by `KeyboardEvent.code`. `KeyA` is the same physical key on every keyboard layout. |
| `pointer.primary` | A left click or a tap. (`pointer.secondary`: a right click.) |
| `mouse.left` | A left click only, not a tap. Also `mouse.middle`, `mouse.right`. |
| `mouse.wheelup`, `mouse.wheeldown` | A step of the mouse wheel: a press and a release for each notch, and for every stretch of a trackpad swipe. |
| `touch.tap` | A tap only, not a click. |
| `pad.south` | The bottom face button of either cluster, on any gamepad or on-screen pad. |
| `pad.right.south` | The bottom right face button (A on Xbox, cross on PlayStation). |
| `pad.left.south` | D-pad down. |
| `pad.left.bumper`, `pad.right.trigger` | Shoulder buttons. |
| `pad.start`, `pad.select` | The menu buttons. |
| `pad.left.stick` | The left thumbstick. Sticks are read with `sdk.input.onStick`, not bound to actions. |
| `controls.<name>` | An on-screen control with no physical equivalent. |

Positions use compass words (`north`, `south`, `east`, `west`) and clusters are
`left` and `right`, because controller brands disagree about which button is
"A". `Escape` can't be bound: hosts use it to leave fullscreen and pointer
lock. The controller's home button can't either, and holding `pad.start` opens
the host's sheet; a tap of Start is still the game's.

Limits: 64 actions, 8 bindings per action, 8 control layouts.

#### Several players on one device

```json
"players": { "local": 4, "online": 8 },
"input": {
  "actions": { "pause": { "bind": ["key.KeyP", "pad.start"] } },
  "player": {
    "actions": {
      "bomb": { "bind": ["pad.south"], "title": "Drop bomb", "join": true }
    },
    "keyboard": [
      { "title": "Left keys", "bind": { "bomb": ["key.Space"] } },
      { "title": "Arrows", "bind": { "bomb": ["key.Enter"] } }
    ]
  }
}
```

- **`input.player.actions`**: what each local player does, a copy each. Their
  `bind` holds pad buttons and on-screen controls only, the same for every
  pad. An id is never one `input.actions` declares too. `join: true` marks the
  actions that seat a new player; on none, any does.
- **`input.player.keyboard`**: the parts of the keyboard players share, one
  player each (at most 4): a `title` and the keys and mouse buttons of each
  player action. A key in two parts is refused.
- **`players.local`**: the most players on one device, 1 by default; more
  than 1 needs `input.player`.

Limits: 32 player actions.

### audio

```json
"audio": {
  "channels": {
    "music":   { "title": "Music" },
    "effects": { "title": "Effects" }
  }
}
```

`audio` is required. Set it to `null` for a game that makes no sound, and to an
object for one that does. `channels` is optional and may be empty: a game with
sound but no channels (`"audio": {}`) gets a mute button and a master volume,
and must still respect them.

Each channel gets its own labelled volume slider in the host's sound controls.
`title` is required: it is the slider's label. `master` is reserved, since
every host already has a master volume. At most 8 channels.

### genres

```json
"genres": ["puzzle", "shooter", "online-multiplayer"]
```

What kind of game this is, the main one first: at least one, and at most 3
beside the two multiplayer words, none twice. A game with none declared is
filed by what its title and description say, and most often less well.

The words are `racing`, `shooter`, `platformer`, `puzzle`, `arcade`, `physics`,
`roguelike`, `strategy`, `building`, `idle`, `sports`, `horror`, `stealth`,
`fighting`, `casual`, `simulation`, `adventure`, `rhythm` and `rpg`, and two
that say how it is played together:

- **`local-multiplayer`**: several people on one device. Needs `players.local`
  above 1.
- **`online-multiplayer`**: over the network. Needs `previews.network`.

Either of those without what it needs is not refused: the game is published,
that word is ignored, and `hopple check`, `hopple upload` and `hopple publish`
warn about it. They warn the other way round too, for a game that declares
`genres` and leaves out a way of playing together its manifest shows, since it
then is not listed as one. Clients group the two under one shelf.

### previews

```json
"previews": { "network": 1 }
```

The parts of the SDK that are not stable yet, each at the revision the game
is written against. A preview is its own `0.x` package, versioned apart from
`api`: revision `R` is `@hopple/sdk-preview-network` `0.R.x`. A game is served a runtime
with the previews it declares, at those revisions, so a change to a preview
reaches only the games written against it. A breaking change is a new
revision. A name nobody knows is refused, and a revision is a whole number
from 1.

Left out of a config, the CLI fills each preview from its installed package,
for a project whose `package.json` lists it, as it fills `api`, and refuses
one that differs from the installed revision, or that names one with none
installed. Publishing a game that declares any preview needs what `next`
needs. Once a newer revision of a preview is out, the older one is retired:
games whose `latest` is on it are unlisted and lose `beta`, keeping their
approval and links, and an upload declaring it is refused.

- **`network`**: the game plays over the network, in matches: `sdk.match`,
  typed by `@hopple/sdk-preview-network`. It is the permission a reviewer grants, and what
  lets a host offer *Play with friends* before the game has loaded. Nothing
  else is declared: how a match is played, and its size, are chosen when the
  game makes one.
  Without it, a game is never told about a match, and `sdk.match` is not
  there.

## Changing the manifest in a new version

Every board and every achievement has a `version`, 1 when left out. The
server refuses a new version of the game whose manifest would change what data
players already have means, and the way out is always to raise that board's or
achievement's `version`: it starts again from nobody under the same id, and
what was stored under the version before is kept but no longer counted.

- **Boards and achievements can be added and dropped freely.** What players
  stored under one that is dropped is kept, and comes back if it is declared
  again.
- **A board's title can change, and so can its format between `integer` and
  `decimal`.** Once it has scores, flipping its `direction` or moving its
  format to or from `duration-ms` needs a new `version`.
- **An achievement's title, detail, icon and secret flag can change, and so can
  its target; tiers can be added after the last one.** Progress is kept as the
  number reported, so a player at 12 of 12 is at 12 of 25 when the target is
  raised, and keeps the unlock; when it is lowered past them, they have it.
  Turning a flag into a measured achievement or back, flipping `direction`,
  moving its format to or from `duration-ms`, or moving or dropping a tier it
  already has needs a new `version`, whether or not anybody holds it yet.
- **A version declared again is held to what it was**, since what was stored
  under it comes back.

Input action ids and audio channel ids may change freely. A player's remapped
keys or volume settings for an id that no longer exists are simply dropped.

