Skip to content

Writing pages

The wiki is the reference for how these games work. Write it as an engine team writes its own documentation: what the engine does and why, then the exact detail, with every statement marked by how surely it is known.

src/content/docs/
engines/<engine>/index.md the engine's overview
engines/<engine>/<system>/<topic>.md a topic of one system (archives, geometry, ... see How this wiki works)
games/<game>.md a game's hub
concepts/<concept>.md an idea across engines
methods/<method>.md a way of finding things out
  • By engine, never by game. A fact about Sekiro’s meshes goes in engines/fromsoft/geometry/, with :only[sekiro] if Elden Ring differs. Two games that do one thing two ways get one page with both, not two pages.
  • One topic per page, named for the thing (flver.md, vertex-formats.md, bhd5-bdt.md), not for a task or a date. A page that passes ~600 lines splits by sub-topic.
  • The engine and system come from the folder. Folders under an engine are its systems, from the fixed list in src/data/engines.yaml. A new game or engine is a line in src/data/games.yaml or engines.yaml.
  • File and folder names are lower-case words joined by -.
---
title: BHD5 and BDT archives
description: "One sentence: what the page covers."
games: [sekiro, elden-ring] # optional: defaults to every game of the engine
sources: # outside work the page relies on (tools, their source, community docs)
- title: SoulsFormats
url: https://github.com/JKAnderson/SoulsFormats
note: BHD5 reader; the field names used here
implementations: # optional: tools that implement what the page describes (shown small at the end)
- name: fromsoft-formats (bhd.rs)
url: https://github.com/Kalmarv/fromsoft-formats/blob/main/src/bhd.rs
---

The build checks every value: unknown game ids, engines, systems and malformed fields fail it. Quote a value that contains : (YAML reads it as a nested mapping otherwise).

  1. Start with what the thing is for and how the engine uses it, in a short paragraph a newcomer can follow.
  2. Then what the games share: structure, layouts, rules, values, each with its badge. One layout for a structure the games share; a field that differs is limited to its games inside it (games on the field), not split into a second layout.
  3. Then a section per game (## Elden Ring, ## Sekiro), in release order, for what is that game’s alone: anything longer than a sentence or two, such as another container version, an extra stage or a different file set. A game’s section holds only how it differs from the shared part and what only it has; it never repeats the shared part. A game with nothing of its own has no section.
  4. Open questions last, or beside the section they belong to.

Where each kind of difference goes:

Difference Where
One sentence or one table row (another constant, a flag one game uses) Inline, where it belongs, with a scope chip or games on its badge
Values that line up field by field across the games (versions, limits, counts) A table in the shared part with a column per game
A field one game adds or reads differently games on that field in the shared layout
More than a couple of sentences, or a structure only one game has That game’s section

A reader looking for “what is special about Sekiro” reads the shared part once and then Sekiro’s section, not every paragraph for its chips.

Write in plain sentences and tables. State facts in the present tense about the game (“the record is 40 bytes”), not as history (“we found that…”). No first-person narrative, no dates of discovery, no task status.

  • Who found something, or which internal tool, repository, test or agent did. Say how it is known (the method and scope), not where in our code it lives. Our tools may appear under implementations: and nowhere else.
  • Local paths, machine names, account names, keys or secrets. Say where a file lives inside the game (“regulation.bin beside the executable”) and describe where to get a key (“published by UXM”) without printing it.
  • Plans, progress, to-dos and conversion choices. The wiki records how the game works, not what a converter decided to do with it.

Shader disassembly, byte values, constants and formulas are fine: they document how the engine works. Do not paste whole game files or large blocks of game data.

Put a badge after every statement of fact, at the end of its sentence or table cell. See Confidence for what earns each level.

Write Shows
:proven Proven
:observed Observed
:sourced[SoulsFormats] Sourced · SoulsFormats
:inferred Inferred
:unknown Unknown
:observed{games="sekiro"} Observed · Sekiro
:proven[every entry]{games="sekiro elden-ring"} Proven · Sekiro, Elden Ring · every entry

The text in [...] is a short scope note. {games="..."} limits the badge to those games; without it, a badge counts for every game the page applies to. Every badge feeds the coverage page.

A paragraph that makes one claim needs one badge. A table of facts takes a badge per row (a Confidence column).

Write Shows Means
:only[elden-ring] only in Elden Ring Elden Ring does this; the others don’t
:since[sekiro] since Sekiro From this game on (in release order)
:not[sekiro] not in Sekiro Every game of the engine but these
:in[sekiro elden-ring] in Sekiro, Elden Ring These games, when the page covers more

These say where a statement applies; the badge says how surely. They are separate on purpose.

A structure’s fields go in a fenced block with the language layout, written in YAML. It is drawn as a byte grid and a field table, checked (no overlapping fields, sizes that fit, known levels and games), and counted on the coverage page.

```layout
title: BHD5 header
size: variable # a byte count, or a description when it varies
games: [sekiro] # optional: the games this layout is for (default: the page's)
note: Little-endian when the flag at 4 is 0xFF.
fields:
- {at: 0, size: 4, type: "char[4]", name: magic, value: "BHD5", level: proven}
- {at: 4, size: 1, type: u8, name: endianness, level: proven, note: "0xFF little-endian, 0 big-endian"}
- {at: 5, size: 1, type: u8, name: unknown, level: unknown, note: "0 or 1"}
- {at: 28, size: "salt length", type: "char[]", name: salt, level: sourced}
```
  • at and size are numbers (bytes) or, for variable ones, a short description.
  • level is the confidence in the field’s meaning. A field whose value is always the same but whose purpose is not known is unknown with a note giving the value.
  • games on a field limits it to those games; fields for different games may then share offsets.
  • Bytes inside the stated size that no field covers are drawn dotted as “not described”. Describe them as unknown fields when you know they are there but not what they are: that counts as accounted for.
:::open[What is the boolean at offset 5?]
It is 0 or 1 in every index; nothing seems to depend on it.
:::

Pictures of what a game draws (a pass’s render targets, a frame’s stages) go in a folder beside the page whose name starts with _ (_frames/), as web-sized files (webp, jpg, png or avif, under 600 KB each). Never put game files there: no textures, models or shader bytecode, only pictures rendered from a frame. Each picture is a paragraph: the image, linked relatively with alt text, then its caption, which says what is shown and how it was made displayable (the tone-mapping operator for HDR, how depth was linearised, how packed channels are shown).

:::slides steps through pictures one at a time (the buttons, the range or the arrow keys; without JavaScript they stand one under another). :::pass shows a pass’s inputs, a --- line, then its result:

:::slides[The G-buffer, target by target]
![Albedo](./_frames/gbuffer-albedo.webp)
Target 0, RGBA8 sRGB: albedo, shown as stored.
![Normals](./_frames/gbuffer-normal.webp)
Target 1, RGB10A2: normals, mapped from -1..1 to 0..1.
:::
:::pass[The lighting pass]
![Albedo](./_frames/gbuffer-albedo.webp)
Albedo.
![Depth](./_frames/depth.webp)
Depth, linearised.
---
![Lit](./_frames/lit.webp)
The lit HDR result, tone-mapped with the stated operator.
:::

A picture with no alt text or caption, a missing file, or one over 600 KB fails the check.

Link to other pages with absolute site paths: [DCX](/engines/fromsoft/archives/dcx/). Relative links and links to pages that don’t exist fail the build. Link to outside sources with full URLs.

Notes from investigations, code comments and older documents are the raw material. When moving a fact in:

  1. Re-state it about the game, in the wiki’s voice, without the history of how it was found.
  2. Give it the level its evidence supports today, using the mapping in Confidence. A note with no evidence is at most Inferred; a retired or archived note is a lead, not a fact.
  3. Keep numbers exact and say what they were counted over (“over the 147,289 meshes of build 25394987”).
  4. If two sources disagree, write both with their levels and an open question.

Search before writing, and add to the page that exists rather than starting a near-duplicate.

Terminal window
pnpm wiki search "forge header" # pages ranked by title, description, headings and text
pnpm wiki show engines/anvil/archives/forge
pnpm wiki game riders-republic # every page about a game (or an engine), by system, with badge counts
pnpm wiki open sekiro # the open questions, by page

Each takes --json; pnpm wiki export prints every page and the registries as JSON for indexers.

Terminal window
pnpm install
pnpm check # every page: frontmatter, links, badges, scopes, layouts, public text (seconds, writes nothing)
pnpm check src/content/docs/engines/fromsoft # only these files or folders
pnpm build # the whole site, as it deploys
pnpm dev # http://localhost:4321 to read it

A change is ready when pnpm check and pnpm build pass and the page reads well in the browser. pnpm check is safe to run while others edit the same checkout; pnpm build is not (one at a time). The check also refuses local paths, and, when WIKI_PRIVATE_NAMES_FILE names a file of private names (machines, accounts), those names on whole pages; a name that is also a file or folder name in the repository counts as the wiki’s own word.

Commit, then pnpm wiki publish. It rebases the committed HEAD onto origin/main, checks every line the commits add and their messages for local paths and private names, runs pnpm check and the full build of that exact commit in a clean worktree (.wt/head, so uncommitted pages in a shared checkout can’t hide a broken link), and pushes it to main, which deploys the site. If main moves meanwhile it rebases and checks again. Nothing is pushed unless all of it passes. It works from any branch, worktree or clone, one publish at a time per repository; when the working tree is clean the branch is brought level with what was pushed. --dry-run does everything but the push.

Writers working at the same time each use their own worktree or clone; publish puts their commits in a line.