The player- and developer-facing wiki for the Open Perpetuum MMO server, built with Zola.
License: Apache-2.0 (see LICENSE/).
- Features — how the gameplay systems work (robots, combat, market, missions, ...)
- Content — generated stat catalogs: items (one page per item), ores, plants, deployables, robots, extensions, tech tree, missions, recipes, shop
- Zones — resource generation mechanics, zone index, worked examples, and the zone map (all zones on the x/y grid with their teleport connections)
- Formats — file format and field reference (zone files, stat fields)
The site build needs no database — the generated markdown is committed to git.
All commands run from this directory; the Zola build runs in a Docker container
(pinned ghcr.io/getzola/zola:v0.23.6), so no local Zola install is required.
make build # build the static site into public/ (runs make i18n + i18n-check first)
make serve # live-reload server on http://localhost:8085 (WIKI_PORT=NNNN to override)
make clean # remove public/The site is available in English (the source language) and 13 more — Čeština,
Deutsch, Español, Français, Italiano, 日本語, 한국어, Nederlands, Polski,
Português, Русский, 简体中文 and 繁體中文. All translatable strings (UI chrome,
game names, generated content) live in translations/<lang>.json; the
per-language pages are generated into content/<lang>/ at build time.
Adding a new language: drop a translations/<lang>.json in place and run
make build. The file must carry the same keys as translations/en.json
(make i18n-check enforces this — missing keys and empty values fail the
build); start from en.json and translate the values. No further wiring:
tools/gen_i18n.py picks the new file up automatically, so the topbar
language menu, the /<lang>/ routes and the translated menu labels in the
search results all appear with it. Game display names go under the "names" key, extracted from
the client's per-language string file (see below).
The file format, the translation conventions (in-game terms stay English,
{lang} placeholder exception, mermaid \n), the name-extraction workflow,
and the test setup are documented in
TRANSLATIONS.md.
make test # Playwright suite (tests/) against the built site.
# One-time setup: npm install && npx playwright install chromium
make test-py # Python generator tests (tests_py/) with coverage
# (pytest + coverage; the in-place idempotency tests need the
# working tree in a generated state — plain `make build` is enough)
make i18n-check # translation-file CI gate (key parity, placeholders, empty values)The i18n machinery (translation files, generators, conventions, and the Python/browser test suites) is documented in TRANSLATIONS.md.
Only needed when the game database changes. Requires the .NET 8 SDK, a reachable game database, and the plant rule files from a GameRoot:
make generate \
WIKI_DB="Server=...;Database=perpetuumsa;User Id=sa;Password=...;TrustServerCertificate=True" \
WIKI_PLANTRULES=/path/to/GameRoot/plantrulesClient display names are a static snapshot of the official client's English
string dictionary (generator/Perpetuum.WikiGenerate/ClientNames.cs, 4,266
entries) — no client archive is needed. The snapshot predates the end of
official client development, so it does not need refreshing; entities the
client never named fall back to a name derived from the internal identifier.
Commit the regenerated pages — the site build always uses the committed markdown.
See generator/README.md for what each generated page is built from, and idea.md for the original design notes.
The zone teleport maps (static/zonemaps/*.svg) show each zone's real
terrain. The layer data is not in this repo — only the small SVGs are
committed, plus derived 512×512 PNGs for the zones whose layer data is not
fetchable in CI (see below). The PNGs for the other zones are generated at
build time by tools/gen_zone_teleport_maps.py (pure standard-library
Python, one process per core) from the game's layer files, sourced in order:
- a local PerpetuumServer2 checkout (sibling
../PerpetuumServer2or theserver/submodule) when it hascustom-layers/—make zonemapsuses it directly, no download - otherwise
tools/fetch_zone_layers.shfetches what is publicly available into.assets/(the same sources the PerpetuumServer2 CI uses): the original zones'.binlayers from the Dedicated Server installer (Steam app 693060, anonymous) and the latest gamma/custom zones from the public Google Drive archive
The remaining zones (classic 2048×2048 worlds) ship only in the game
client's Perpetuum.gbf, which needs a Steam account — not available to CI
yet. For those, static/zonemaps-fallback/<zone>/{height,color}.png holds
previously derived terrain PNGs committed to the repo (~11 MB); the tool
keeps them instead of downgrading to the procedural placeholder. Once a
Steam account can fetch the client data, regenerate locally and delete the
fallback directory.
After re-running make generate (new zones or moved teleports), re-run
make zonemaps to refresh the map backgrounds and links.
The .NET generator is the long-term source of the generated pages, but a few
tools/ scripts transform the already-committed markdown without a database —
use them when the .NET toolchain is not at hand. Each one is idempotent and
kept in sync with its generator counterpart (the page it feeds):
| Tool | What it does | Generator counterpart |
|---|---|---|
tools/gen_production_pages.py |
adds the "Production" (components → item) and "Used in production" (item → products) mermaid sections to item pages from the tools/recipes_data.json cache, which the .NET generator writes from the components/itemresearchlevels tables |
ItemsPage.cs |
tools/gen_recipes_cards.py |
lays out the recipes page as category sections of reward-style cards, each linked to the item's page | RecipesPage.cs |
tools/gen_extension_categories.py |
builds static/extensions-categories.svg (the 15 skill categories with cross-category prerequisite arrows, each box linking to its section) and embeds it inline in the extensions page's "Main categories" section |
ExtensionsCategories.cs |
tools/regen_search_index.py |
rewrites static/search_index.json from the committed pages (title/description/URL per page) |
SearchIndex.cs |
Run order for a full refresh: gen_production_pages.py, then
gen_recipes_cards.py and gen_extension_categories.py (independent), then
regen_search_index.py, then rebuild.
GitHub Actions in .github/workflows/ are pinned to commit SHAs with
ratchet (the # ratchet:... comment
records the original tag constraint). CI lints the pins on every run; to
refresh them to the latest matching tags:
docker run --rm -v "${PWD}:${PWD}" -w "${PWD}" ghcr.io/sethvargo/ratchet:latest update .github/workflows/wiki.yml