Custom Registry Developer Guide

Define custom bundle-backed registries, populate them from JSON, and query them from Lua.

Custom Registry Developer Guide

Custom registries let you ship your own structured data sets in bundles without adding a new built-in Kotlin registry type. The engine loads a registry definition from the built-in registries registry, then loads that registry's entries from bundle JSON files.

When to use a custom registry

Use a custom registry when:

  • You want bundle data to be addressable by identifier, like mygame:quests/main_story
  • You want hot-reload support for registry files during development
  • You want to query entries from Lua through selene.registries
  • You do not need a dedicated typed Kotlin serializer for each entry

Custom registries store each entry as raw JSON and expose it as a CustomRegistryObject in Lua.

How custom registries are declared

Custom registry definitions live in the built-in registries registry on the common platform. In practice, that means your bundle declares them in:

common/data/<namespace>/registries.json

The merged file format uses an entries object:

{
  "entries": {
    "mygame:quests": {
      "name": "quests",
      "platform": "server"
    },
    "mygame:music_presets": {
      "name": "music_presets",
      "platform": "client"
    }
  }
}

Each definition has:

  • name: the directory or merged-file name used on disk for this registry
  • platform: where the registry entries are loaded from, currently client or server

The key inside entries is the registry identifier. That identifier is what Lua uses to look the registry up.

Identifier vs storage name

There are two names involved, and they serve different purposes:

  • Registry identifier: mygame:quests
  • Storage name: quests

The identifier is used when you query the registry from Lua:

local Registries = require("selene.registries")
local quest = Registries.findByName("mygame:quests", "mygame:introduction")

The storage name is used for the JSON path on disk:

  • server/data/mygame/quests.json
  • server/data/mygame/quests/introduction.json

If you only pass "quests" to selene.registries, the engine will parse it as selene:quests, which is not your custom registry. Use the full identifier such as "mygame:quests".

Where registry entries go

Once a custom registry is declared, its entries are loaded from the platform named in the definition.

For a server registry named quests in namespace mygame, the engine looks in:

  • server/data/mygame/quests.json
  • server/data/mygame/quests/**/*.json

For a client registry named music_presets in namespace mygame, the engine looks in:

  • client/data/mygame/music_presets.json
  • client/data/mygame/music_presets/**/*.json

The same rule applies to any namespace and registry name.

Two supported file layouts

FileBasedRegistry supports both a merged JSON file and a directory tree of per-entry JSON files.

Option 1: merged file

Use a single file when the registry is small or you want everything together:

server/data/mygame/quests.json

{
  "entries": {
    "introduction": {
      "title": "Introduction",
      "recommendedLevel": 1,
      "metadata": {
        "category": "main",
        "repeatable": false
      }
    },
    "blacksmith_help": {
      "title": "A Favor for the Blacksmith",
      "recommendedLevel": 3,
      "metadata": {
        "category": "side",
        "repeatable": true
      }
    }
  }
}

This produces the identifiers:

  • mygame:introduction
  • mygame:blacksmith_help

Option 2: directory tree

Use per-entry files when the registry is large or you want nested paths:

  • server/data/mygame/quests/introduction.json
  • server/data/mygame/quests/chapters/chapter1.json

server/data/mygame/quests/introduction.json

{
  "title": "Introduction",
  "recommendedLevel": 1,
  "metadata": {
    "category": "main",
    "repeatable": false
  }
}

server/data/mygame/quests/chapters/chapter1.json

{
  "title": "Chapter 1",
  "recommendedLevel": 5,
  "metadata": {
    "category": "main",
    "chapter": 1
  }
}

This produces the identifiers:

  • mygame:introduction
  • mygame:chapters/chapter1

Lua access

Custom registries are queried through selene.registries.

local Registries = require("selene.registries")

local allQuests = Registries.findAll("mygame:quests")
local introQuest = Registries.findByName("mygame:quests", "mygame:introduction")
local firstMainQuest = Registries.findByMetadata("mygame:quests", "category", "main")

For custom registries, these functions return CustomRegistryObject instances.

Reading fields and metadata in Lua

CustomRegistryObject exposes the identifier plus helper methods for raw JSON access.

local Registries = require("selene.registries")

local quest = Registries.findByName("mygame:quests", "mygame:introduction")
if quest then
  print(quest.Name)
  print(quest:GetField("title"))
  print(quest:GetField("recommendedLevel"))
  print(quest:GetMetadata("category"))
end

Use:

  • Name for the object's full identifier string
  • GetField("key") for top-level JSON fields
  • GetMetadata("key") for values inside the metadata object

findByMetadata only searches values stored inside the entry's metadata object.

Hot reload behavior

Custom registries participate in the same file watching flow as other bundle-backed registries.

  • Updating common/data/<namespace>/registries.json reloads the registry definitions
  • Updating a merged registry file like server/data/mygame/quests.json reloads the whole custom registry
  • Updating a single file under server/data/mygame/quests/ reloads just that entry

If both a merged file and a directory exist for the same registry in the same namespace, the merged file is authoritative.

For most bundles:

  1. Declare the registry once in common/data/<namespace>/registries.json
  2. Put the actual entries under client/data/... or server/data/...
  3. Use merged files for small registries
  4. Use directory trees for large registries or when nested identifiers are useful
  5. Put searchable tags in metadata if you plan to use findByMetadata

Migration note

Older bundle examples may use a different schema with fields like suffix. The current engine code in /home/blay/Development/Selene expects:

{
  "name": "quests",
  "platform": "server"
}

If you are writing new bundles, follow the name and platform format shown in this guide.

Troubleshooting

  • Registry lookup fails in Lua: use the full registry identifier, for example mygame:quests, not just quests
  • Entry lookup fails: use the full entry identifier such as mygame:introduction
  • findByMetadata returns nothing: make sure the value is under metadata, not only at the top level
  • File changes do not behave as expected: do not mix a merged file and per-entry files for the same registry unless you intend the merged file to override the directory