Explanation: The Registry Lifecycle
Understanding the order in which Library of Exile initializes its registries helps you avoid timing bugs — registering content too early, reading entries before they exist, or missing the sync window.
Phase 1 — Mod construction (@Mod constructor)
Each mod's @Mod-annotated constructor runs during Forge's mod loading phase. At this point:
OrderedModConstructor.register()is called.- Forge
DeferredRegisterinstances are bound to the event bus (registerDeferredContainers). - Forge
DeferredRegisterentries — items, blocks, entity types — are registered into those containers (registerDeferredEntries). - New
ExileRegistryContainerinstances are added toDatabase(registerDatabases). ExileRegistryEventClassandExileKeyHolderinstances are set up but not yet executed.
At the end of this phase, Database knows which containers exist, but no content entries have
been added yet.
Phase 2 — Forge object registration
Forge processes all DeferredRegister entries and populates ForgeRegistries. After this phase,
ForgeRegistries.ITEMS.getValue(...) works correctly.
Library of Exile's own items and blocks are registered here.
Phase 3 — FMLCommonSetupEvent (hardcoded entries)
The library fires ExileEvents.EXILE_REGISTRY_GATHER once for each ExileRegistryType, in
ascending order value.
For each fired event:
- All ExileRegistryEventClass listeners whose getType() matches add their entries via
e.add(...) or e.addSeriazable(...).
- All ExileKey factories registered by ExileKeyHolder.init() run and register their entries.
- registerDatabaseEntries() from each OrderedModConstructor runs (it was deferred to this
point).
After this phase, all hardcoded entries are in Database.
Phase 4 — Datapack loading (JSON entries)
This happens after FMLCommonSetupEvent, triggered by Forge's AddReloadListenerEvent. It
runs:
- On initial world load.
- On every /reload command.
- On player join (only the sync packet side — JSON was already loaded).
For each ExileRegistryType that has a non-null serializer, BaseDataPackLoader scans the
appropriate datapack folder and parses each JSON file.
After all loaders finish, CommonInit.onDatapacksReloaded() runs:
1. GUID validation — invalid GUIDs are logged and removed.
2. Invalid entries pruned.
3. ON_LOGIN packet bytes cached per container.
4. ExileEvents.AFTER_DATABASE_LOADED fired for any post-load hooks.
Phase 5 — Player login (sync)
When a player connects (or after /reload while players are online), OnDatapackSyncEvent
fires. For every ON_LOGIN type, the pre-cached packet bytes are sent to the client.
The client's Database is populated with the received entries. From this point, client-side
code can safely call Database.get(...) for ON_LOGIN types.
Timing rules
| Operation | Earliest safe moment |
|---|---|
Register a DeferredRegister with the event bus |
@Mod constructor |
| Register a Forge item/block | @Mod constructor (via DeferredRegister) |
Call ForgeRegistries.ITEMS.getValue(...) |
After FMLCommonSetupEvent |
Call Database.get(...) for a hardcoded entry |
After FMLCommonSetupEvent |
Call Database.get(...) for a JSON entry |
After datapack loading (post-AFTER_DATABASE_LOADED) |
Call Database.get(...) on the client |
After the first ON_LOGIN sync |
Re-entrant datapack loading
Because /reload can happen while the server is running, the datapack loading phase (Phase 4)
can execute multiple times in one server session. Hardcoded entries (Phase 3) are not re-run on
/reload — they stay fixed until the JVM restarts. JSON entries are fully replaced on each
reload.
This means:
- If you remove a JSON file and run /reload, that entry disappears from Database.
- If you add a JSON file and run /reload, it appears immediately.
- Hardcoded entries cannot be changed without a restart.