{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://play.unikia.es/schema/unihub.manifest.v1.json",
  "title": "Uniplay Hub game manifest",
  "description": "unihub.manifest.json: how a game describes itself to the Uniplay Hub. It sits at the root of the game's folder and of its submission zip.",
  "type": "object",
  "required": ["manifestVersion", "id", "title", "version", "developers", "description", "genre", "tagline", "players"],
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "This schema's URL, for editor autocomplete: https://play.unikia.es/schema/unihub.manifest.v1.json"
    },
    "manifestVersion": {
      "const": 1,
      "description": "The manifest format. Always 1.",
      "errorMessage": "must be 1"
    },
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$",
      "maxLength": 40,
      "description": "The stable game id: a-z, 0-9 and -. Players' saves and achievements are stored under it, so never change it once published.",
      "errorMessage": "must use a-z, 0-9 and - (at most 40 characters), starting with a letter or digit"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40,
      "pattern": "\\S",
      "description": "The game's name in the Hub, at most 40 characters.",
      "errorMessage": "is required: the game's name, at most 40 characters"
    },
    "version": {
      "type": "string",
      "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$",
      "description": "The game's version, semver (e.g. 1.2.0). Shown in Game details. This one wins over package.json's.",
      "errorMessage": "must be a semver version such as 1.2.0"
    },
    "developers": {
      "type": "array",
      "minItems": 1,
      "maxItems": 10,
      "uniqueItems": true,
      "items": { "type": "string", "pattern": "^[a-z0-9_]{3,20}$" },
      "description": "Hub nicknames of the people who make the game, credited as its Developers in Game details. The game is uploaded by a publisher, which needn't list its members here.",
      "errorMessage": "must list 1 to 10 Hub nicknames (3 to 20 of a-z, 0-9 and _), without duplicates"
    },
    "description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 160,
      "pattern": "\\S",
      "description": "One-line pitch shown under the title, at most 160 characters.",
      "errorMessage": "is required: a one-line pitch, at most 160 characters"
    },
    "tagline": {
      "type": "string",
      "minLength": 1,
      "maxLength": 60,
      "pattern": "\\S",
      "description": "A short slogan on the artwork, at most 60 characters. The Hub shows it in capitals.",
      "errorMessage": "is required: a short slogan, at most 60 characters"
    },
    "details": {
      "type": "string",
      "minLength": 1,
      "maxLength": 600,
      "pattern": "\\S",
      "description": "Longer text for Game details, at most 600 characters. Defaults to description.",
      "errorMessage": "must be text, at most 600 characters"
    },
    "genre": {
      "type": "string",
      "minLength": 1,
      "maxLength": 24,
      "pattern": "\\S",
      "description": "The one main genre, e.g. \"Shooter\" or \"Roguelite\", at most 24 characters. Everything else goes in tags.",
      "errorMessage": "is required: one main genre, at most 24 characters"
    },
    "tags": {
      "type": "array",
      "maxItems": 12,
      "items": { "type": "string", "minLength": 1, "maxLength": 24, "pattern": "\\S" },
      "description": "Up to 12 more descriptors, most important first: sub-genres, modes, setting, style (e.g. \"Co-op\", \"Pixel art\", \"2D\"). Shown as written.",
      "errorMessage": "must be a list of up to 12 tags, each at most 24 characters"
    },
    "players": {
      "description": "How many can play: a number is exactly that many (1 for single player); a range { \"min\": 1, \"max\": 4 } is solo or with up to four.",
      "oneOf": [
        { "type": "integer", "minimum": 1, "maximum": 64 },
        {
          "type": "object",
          "required": ["min", "max"],
          "additionalProperties": false,
          "properties": {
            "min": { "type": "integer", "minimum": 1, "maximum": 64 },
            "max": { "type": "integer", "minimum": 1, "maximum": 64 }
          }
        }
      ],
      "errorMessage": "must be a number of players (1 for single player) or a range like { \"min\": 1, \"max\": 4 }"
    },
    "platforms": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": { "enum": ["desktop", "mobile"] },
      "default": ["desktop"],
      "description": "Devices the game is made for: \"desktop\" (keyboard, mouse or controller) and \"mobile\" (touch alone). Defaults to [\"desktop\"].",
      "errorMessage": "must list one or more of desktop, mobile, without duplicates"
    },
    "inputs": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": { "enum": ["keyboard", "gamepad", "touch"] },
      "description": "How the game can be played, start to finish, shown in Game details: \"keyboard\" (keyboard, with or without a mouse), \"gamepad\" (a controller) and \"touch\". Defaults from \"platforms\": desktop is keyboard, mobile is touch.",
      "errorMessage": "must list one or more of keyboard, gamepad, touch, without duplicates"
    },
    "languages": {
      "type": "array",
      "minItems": 1,
      "maxItems": 40,
      "uniqueItems": true,
      "items": { "type": "string", "pattern": "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$" },
      "default": ["en"],
      "description": "Languages the game can be played in, as BCP 47 codes like \"en\", \"es\" or \"pt-BR\", the original language first. Defaults to [\"en\"].",
      "errorMessage": "must list one or more language codes like en, es or pt-BR, without duplicates"
    },
    "fullscreen": {
      "type": "boolean",
      "default": false,
      "description": "Whether the game asks to be played fullscreen, such as a first-person shooter. When it starts, the Hub (and Uniplay for desktop) goes fullscreen, unless the player turned that off in Settings, and comes back when they leave the game. Defaults to false.",
      "errorMessage": "must be true or false"
    },
    "coopAcrossVersions": {
      "type": "boolean",
      "default": false,
      "description": "Whether buds on different versions of the game can play together. By default they can't: the Hub only shows a game the peers on its own version, and offers the one behind an update before joining. Set it to true when versions stay compatible (no change to what the game sends). Defaults to false.",
      "errorMessage": "must be true or false"
    },
    "playthroughs": {
      "type": "boolean",
      "default": true,
      "description": "Whether Game details offers \"Start a new game\", which puts the player's saves aside as another playthrough to switch back to later. Set it to false for a game that keeps its own save slots (characters, save files), where players start over inside the game. Defaults to true.",
      "errorMessage": "must be true or false"
    },
    "color": {
      "type": "string",
      "pattern": "^#[0-9a-fA-F]{6}$",
      "description": "Accent colour, #rrggbb.",
      "errorMessage": "must be a #rrggbb colour"
    },
    "cover": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.(webp|png|jpg)$",
      "description": "Path to the game's 16:9 key art (.webp, .png or .jpg), relative to this file. The Hub shows it as is, so include the game's logo in it. A game needs this, \"squareCover\" or both.",
      "errorMessage": "must be a relative path to a .webp, .png or .jpg file"
    },
    "squareCover": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.(webp|png|jpg)$",
      "description": "Path to the game's 1:1 key art (.webp, .png or .jpg), relative to this file, for the Hub's grid view. The Hub shows it as is, so include the game's logo in it. A game needs this, \"cover\" or both.",
      "errorMessage": "must be a relative path to a .webp, .png or .jpg file"
    },
    "badge": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.(webp|png|jpg)$",
      "description": "Path to the game's platinum badge (.webp, .png or .jpg, 1:1, ideally a 256x256 .webp with a transparent background), relative to this file. Players earn it by unlocking every achievement; the Hub shows it greyed out until then. Without one, the Hub makes a badge from the game's cover.",
      "errorMessage": "must be a relative path to a .webp, .png or .jpg file"
    },
    "entry": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.html$",
      "default": "dist/index.html",
      "description": "The built game's HTML file, relative to this file. Its folder is served at /<path>/. Defaults to dist/index.html.",
      "errorMessage": "must be a relative path to an .html file"
    },
    "achievements": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.json$",
      "description": "Path to the game's achievements.json, relative to this file. See https://play.unikia.es/schema/achievements.v1.json.",
      "errorMessage": "must be a relative path to a .json file"
    },
    "changelog": {
      "type": "string",
      "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+\\.md$",
      "description": "Path to the game's release notes in the Keep a Changelog format, relative to this file. Game details shows them from the version. 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 language, else this one.",
      "errorMessage": "must be a relative path to a .md file"
    },
    "sdk": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[0-9xX*^~<>=|. -]+$",
      "description": "The range of the Uniplay SDK the game was built against, e.g. ^1.4.0.",
      "errorMessage": "must be a version range such as ^1.4.0"
    },
    "path": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$",
      "not": { "enum": ["admin", "api", "assets", "developers", "g", "healthz", "library", "licenses", "policy", "publisher", "schema", "uniplay"] },
      "description": "The URL folder the game is served from, /<path>/. Defaults to the id. The Hub may assign another one on review to avoid clashes.",
      "errorMessage": "must use a-z, 0-9 and -, and not be a path the Hub keeps for itself (admin, api, assets, developers, g, healthz, library, licenses, policy, publisher, schema, uniplay)"
    }
  }
}
