Files
ark-stronghold-protocol/docs/PACKS.md
T

10 KiB

PACKS.md — content packs

A content pack is add-on content a server picks up from a folder: a manifest plus its files. The owner's decision of 2026-10-07 made the language pack the first type of one general mechanism, so that later resource packs (art, audio, fonts) and server-side data packs reuse the same manifest, registry and index instead of each inventing its own. In 0.2.0 one type loads, lang (a language: docs/I18N.md "Adding a language"); assets and data are planned.

File Role
shared/packs.js the format: the manifest, the pack types (PACK_TYPES), app version ranges, the index (browser, server and tools)
shared/i18nPacks.js the lang type: language codes, the language fields, the fallback chain, the script family
server/packs.js the registry: discovery, validation, the index, which pack files may be served
server/http/static.js GET /packs/index.json (the live index) and GET /packs/<id>/<file>
public/js/ui/lang.js the client of the lang entries: the language menu and the loader
tools/packs.mjs list, index (the index file for a static host), check
tools/package.mjs ships the committed packs and writes packs/index.json into the release zips

1. Layouts

Two layouts, read by the same registry and listed alike:

packs/<id>/pack.json + its files      a folder pack — any type; the manifest names its files
public/i18n/<code>.json               a single-file language pack — its manifest is the file's `_meta` block
data/i18n/<code>.json                   (+ its game texts, when present)

The single file is the quickest way for a translator (one file, the format English has always had, and how English ships); the folder is the layout every pack type shares — a pack distributed as one folder with its own readme and licence, and the only layout a resource or data pack will have. A language can come either way; the menu does not tell them apart.

The id of a folder pack is its folder name; the id of a single-file language pack is its language code. Ids are letters, digits, ., _ and - (at most 64), unique across types and without regard to case (Windows and macOS would see one folder).

2. The manifest

packs/<id>/pack.json, or the _meta block of public/i18n/<code>.json (the same fields):

{
  "type": "lang",
  "lang": "ja",
  "name": "日本語",
  "englishName": "Japanese",
  "version": "1.0.0",
  "app": ">=0.2.0",
  "authors": ["…"],
  "credits": "…",
  "license": "CC-BY-4.0",
  "fallback": ["en"],
  "files": { "ui": "ui.json", "data": "data.json" }
}
Field Meaning
id optional: the folder or file name decides; a manifest that names another id gets a warning
type lang (a single-file language pack may leave it out); planned: assets, data
version the pack's own version
app the app versions it is made for, a range: >=0.2.0, >=0.2.0 <0.3.0, 0.2.x, ^0.2.0, ~0.2.1, *, alternatives with `
name, englishName the name in its own language (a language pack: the language's own name, shown in the menu) and in English (the menu's tooltip)
authors, credits, license who made it; free text (license: an SPDX id or text)
files a folder pack: role → path inside the folder. Each type lists its roles and their extensions (lang: ui required, data optional, both .json)
the type's fields lang: lang, base, fallback, complete, numberUnits (docs/I18N.md)

A problem keeps a pack from loading (unknown type, no lang, a file missing or outside the folder, broken JSON, an id or a language already taken — the language folders' files come first, then the folders by id); a warning does not (an id that differs, an app range this version is outside of, an unknown file role). Both are printed when the server starts or notices a change, and by node tools/packs.mjs list.

3. Discovery and the index

The server's registry (server/packs.js) scans public/i18n/, data/i18n/ and packs/ when it starts and again whenever something there changes: on a request for the index or a pack file it compares the names, sizes and times of the files it reads (at most once a second, a few stat calls) and scans again if they differ. A pack dropped into a folder shows on the next page load — no restart, no build step; a removed one leaves (a player whose stored language is gone gets Chinese).

GET /packs/index.json (never cached) answers the packs of supported types:

{ "version": 1, "app": "0.2.0", "packs": [
  { "id": "en", "type": "lang", "name": "English", "englishName": "English", "version": "0.2.0", "app": ">=0.2.0",
    "compatible": true, "authors": ["…"], "lang": "en", "fallback": [], "complete": true, "strings": 1017,
    "files": { "ui": "/i18n/en.json", "data": "/data/i18n/en.json" } } ] }

files holds URLs on this server, so a client never builds a path itself. A client reads the entries of its type (readPackIndex(json, 'lang'): an entry of another type, with a bad id or a URL that is not a path on this server is dropped).

A static host cannot list folders: node tools/packs.mjs index writes the same body to packs/index.json (git-ignored), and tools/package.mjs writes it into the release zips. The live server answers /packs/index.json itself and ignores that file. Serving the game from plain files needs the server's URL layout: public/ at /, data/ at /data/, shared/ at /shared/, server/sim/ at /sim/ and packs/ at /packs/ (the game itself still needs the Node server for its rooms; DEPLOY.md).

What is served. GET /packs/<id>/<file> answers only a file that the manifest of a loaded folder pack names under a role of its type, with that role's extension, inside the folder once links are resolved — pack.json, a readme, an HTML or script file in the folder are never served; a pack of a planned type serves nothing. A single-file language pack's files are the language folders' own (/i18n/<code>.json, /data/i18n/<code>.json).

4. The types

Type Status Where it applies Loader
lang supported (0.2.0) the client: UI strings, game texts public/js/ui/lang.js (the menu, the strings: shared/i18n.js; the game texts: public/js/data.js)
assets planned the client: art, audio, font replacements —
data planned the server: data patches applied at start —

A pack of a planned type is parsed and listed as not supported ("type "assets" is planned …, not loaded by this version"); it is never served and never changes anything.

Adding a pack type

  1. The type — its entry in PACK_TYPES (shared/packs.js): status, summary, live (whether a running server may take changes without a restart) and files (the roles a folder pack names, with the extensions that may be served for each). Its own manifest fields get a normalizer beside langFields and a branch in normalizeManifest; its index fields a branch in packIndexEntry.
  2. The loader — reads the registry by type: createPackRegistry(…).list('assets') on the server, or the index entries of the type in the browser (readPackIndex(json, 'assets')), the way ui/lang.js reads lang. A server-side type (data) loads once at start (live: false): the simulation's code and data are fixed for the life of the process, and a battle must run the same data on the server and in every browser.
  3. The checks — what tools/packs.mjs check and the tests verify for the type (a data pack: the same validation the data builder applies; a resource pack: the files exist and are of the allowed kinds), and its documentation here.

Things later types must keep (the maintainers' study of mods, 2026-10-07):

  • Server-side only for gameplay. A data pack is installed on the server, so every player of a room uses the same content and the server can still check or recompute a battle. A player never installs gameplay content of their own.
  • A content hash in the handshake before data packs ship (welcome, /healthz, the BattleSpec), so a client and a server can tell they run the same content, and a "modded" mark in the UI and in bug reports.
  • No third-party code. The manifest and its files are data (JSON, images, audio, fonts); a pack type that runs code is a separate decision of the owner's.
  • The golden results stay on the original content; a pack never changes them.

5. Tools

node tools/packs.mjs list [--json]          the packs by type, what was skipped and why, the warnings
node tools/packs.mjs index [--out <file>]   write packs/index.json (the static index)
node tools/packs.mjs check [--strict]       list; --strict exits 1 when a pack is skipped or warned about
node tools/i18n.mjs template <code>         a new language pack (docs/I18N.md)
node tools/i18n.mjs check <code> | --all    a language pack's strings: coverage, placeholders

--root <dir> points tools/packs.mjs at another checkout or an unpacked release.

6. Shipping

tools/package.mjs ships the committed packs (public/i18n/, data/i18n/, packs/ — tracked files only: a pack installed on this machine and not committed stays out) and writes packs/index.json into the stage from the packs that ship. test/package.test.js checks both layouts and that a local pack stays out.

Tests

test/i18n-packs.test.js (discovery of both layouts, broken / planned / duplicate packs, a pack dropped in or removed while the server runs, the file allowlist, the manifest and app ranges, the fallback chain, placeholders, plurals, the per-text game-text fallback, the client loader and menu, the tools, build-i18n --lang / --dict), test/package.test.js, test/ui/i18n.e2e.test.js (SP_E2E=1: a pack file and a pack folder dropped in while the server runs show in the menu and switch).