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 DeferredRegister instances are bound to the event bus (registerDeferredContainers).
  • Forge DeferredRegister entries — items, blocks, entity types — are registered into those containers (registerDeferredEntries).
  • New ExileRegistryContainer instances are added to Database (registerDatabases).
  • ExileRegistryEventClass and ExileKeyHolder instances 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.