Skip to content

Content Registry

The ContentRegistry is a read-only data structure that holds all game definitions. It is built at load time from the content/ directory and is never modified by the engine or renderer during gameplay.

This page is for developers working with the engine API or a custom renderer. You never touch the registry directly while writing content. It is simply where your files end up at runtime.

The registry organizes content by ID so the engine can find it without scanning project files during play.

interface ContentRegistry {
player?: PlayerCharacter;
locations: Record<string, Location>;
characters: Record<string, Character>;
items: Record<string, Item>;
maps: Record<string, Map>;
dialogues: Record<string, Dialogue>;
quests: Record<string, Quest>;
journalEntries: Record<string, JournalEntry>;
interludes: Record<string, Interlude>;
locales: Record<string, LocaleData>;
}

Collections are indexed by each entity’s id. For example, a location with id: tavern is stored at registry.locations.tavern. The player is different: content/player.yaml is a single optional definition stored directly at registry.player, never under registry.characters.

The dev server (npm run dev) builds the registry automatically:

  1. Scans content/ subdirectories
  2. Reads .yaml files as entities based on their directory
  3. Converts .dlg files into dialogue data
  4. Loads the translation entries from each locale file
  5. Serves the complete registry via /api/content
Directory Registry Field Loader
content/locations/*.yaml registry.locations YAML parse, keyed by id
content/characters/*.yaml registry.characters YAML parse, keyed by id
content/items/*.yaml registry.items YAML parse, keyed by id
content/maps/*.yaml registry.maps YAML parse, keyed by id
content/dialogues/*.dlg registry.dialogues DSL parser, keyed by filename
content/quests/*.yaml registry.quests YAML parse, keyed by id
content/journal/*.yaml registry.journalEntries YAML parse, keyed by id
content/interludes/*.yaml registry.interludes YAML parse, keyed by id
content/locales/*.yaml registry.locales YAML parse, keyed by filename
content/player.yaml registry.player YAML parse, optional file

Locale files use their filename as the locale code. For example, en.yaml becomes registry.locales.en.

Dialogue files use the filename without its extension as the dialogue ID. For example, bartender_greeting.dlg becomes registry.dialogues.bartender_greeting.

Each project can have one optional player.yaml file. It defines the player character’s profile fields and starting stats. It has no id, location, dialogue, relationship, or party-membership field. The toolkit records its source as player:player in the file map used by Studio.

game.yaml is loaded separately as a GameConfig, not part of the registry. Its playerCreatesProfile flag decides whether the profile comes from player.yaml or is entered by the player. If the flag and player.yaml are both absent, the engine uses a generic Player profile and does not open the profile modal.

The player data has three distinct forms:

  • registry.player contains the immutable definition loaded from player.yaml, including detailed stat definitions such as { name: "Strength", value: 16 }.
  • GameState.player contains the current profile text and current stat values. Effects update this state, and saves preserve it.
  • snapshot.player contains renderer-ready profile text, localized stat values, and a separate statNames map.

NPCs use the same separation for stats: character YAML provides the stat names and starting values, GameState.characterState[id].stats holds current values, and each SnapshotCharacter exposes stats plus statNames. Keeping the name separate lets game logic address a stable stat key while renderers display a localized label.

Localization applies to player and character profile fields, stat names, and string stat values. Player-entered profile text remains literal.

The registry is passed to the Engine constructor:

const engine = new Engine(registry);

The engine uses the registry to:

  • Look up location data when building snapshots
  • Find character dialogues when talkTo is called
  • Initialize the player profile and character stats for a new game
  • Resolve localization keys and interpolate variables, player profile fields, and character stats at snapshot time
  • Check triggered dialogue and interlude conditions on location change
  • Determine travel distances from map data

In the browser, the registry is fetched from the dev server:

const response = await fetch('/api/content');
const { registry, config } = await response.json();
const engine = new Engine(registry);
const snapshot = engine.newGame(config);

Entities reference each other by ID:

  • Character dialogue field references a dialogue ID
  • Character location field references a location ID
  • GameConfig.playerCreatesProfile selects fixed or player-entered profile text
  • Item location field references a location ID, "inventory", or a character ID
  • Map locations[].id references a location ID
  • Dialogue triggerLocation references a location ID
  • Interlude triggerLocation references a location ID
  • INTERLUDE <id> references an interlude ID
  • GameConfig.startLocation references a location ID
  • GameConfig.startInventory contains item IDs

npm run validate and npm run build check these references before release. At runtime, the engine uses fallback behavior where possible: some actions leave state unchanged, and a missing location produces a fallback snapshot.