Custom Registry Developer Guide
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 registryplatform: where the registry entries are loaded from, currentlyclientorserver
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.jsonserver/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.jsonserver/data/mygame/quests/**/*.json
For a client registry named music_presets in namespace mygame, the engine looks in:
client/data/mygame/music_presets.jsonclient/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:introductionmygame: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.jsonserver/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:introductionmygame: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:
Namefor the object's full identifier stringGetField("key")for top-level JSON fieldsGetMetadata("key")for values inside themetadataobject
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.jsonreloads the registry definitions - Updating a merged registry file like
server/data/mygame/quests.jsonreloads 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.
Recommended layout
For most bundles:
- Declare the registry once in
common/data/<namespace>/registries.json - Put the actual entries under
client/data/...orserver/data/... - Use merged files for small registries
- Use directory trees for large registries or when nested identifiers are useful
- Put searchable tags in
metadataif you plan to usefindByMetadata
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 justquests - Entry lookup fails: use the full entry identifier such as
mygame:introduction findByMetadatareturns nothing: make sure the value is undermetadata, 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