11 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, machineTranslated, numberUnits (docs/I18N.md) |
machineTranslated |
lang, optional: true when the pack's UI strings come from a machine (a translation, or a conversion such as the Traditional Chinese pack's). The index entry carries it (only when true, like complete) and the client shows a note under the language switch in 设置 while that language is in use (public/js/ui/lang.js machineTranslationNote). A value other than true / false is a warning and reads as false |
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
- The type — its entry in
PACK_TYPES(shared/packs.js):status,summary,live(whether a running server may take changes without a restart) andfiles(the roles a folder pack names, with the extensions that may be served for each). Its own manifest fields get a normalizer besidelangFieldsand a branch innormalizeManifest; its index fields a branch inpackIndexEntry. - 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 wayui/lang.jsreadslang. 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. - The checks — what
tools/packs.mjs checkand 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 machineTranslated mark from the manifest
to the 设置 note, 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; the shipped
packs with their UI strings and the 设置 note of a machine-translated one).