Lifecycle and Threads
This page explains when and on which thread your mod code is called. Each item follows the Javadoc of the type and the actual behavior of the Poly Mod Loader runtime.
At a glance
JVM start ─ main thread
│
├─ Mod discovery · poly.mod.json check · dependency resolution POLY-METADATA-*, POLY-DEPENDENCY-*
├─ System replacement conflict resolution (before any mod code) POLY-SYSTEM-001/002/004/005
├─ Transform plan finalized POLY-TRANSFORM-*
├─ SystemProvider.create (once per system) POLY-SYSTEM-003
├─ common entrypoint (all mods, dependency order) POLY-ENTRYPOINT-*
├─ client or server entrypoint (dependency order)
├─ Game layer starts POLY-BOOTSTRAP-005
│
└─ Game main
├─ After Minecraft's bootstrap: other loaders' mods initialise (Fabric), then
│ LifecycleEvents.MODS_INITIALIZED
├─ Just before registry freeze: declarations become game objects, registration closes
│ ── later declarations fail with POLY-*-002 ──
├─ Server thread: events, ticks, commands, AI, scheduled tasks, server-side payloads
├─ Client (render) thread: HUD, screens, drawing, client-side payloads
├─ World generation threads: code placement features, density functions, structures, terrain generators
└─ Network thread: payload codecs
│
JVM exit ─ "Poly Mod Loader Shutdown" thread
└─ onShutdown tasks, in reverse registration order POLY-SHUTDOWN-001
Startup order
| Step | What runs | Thread | Failure |
|---|---|---|---|
| 1 | Mod discovery, metadata check, dependency resolution, system replacement conflict resolution | main | POLY-METADATA-*, POLY-DEPENDENCY-*, POLY-SYSTEM-*. No mod code has run yet |
| 2 | SystemProvider.create of the system being replaced | main, once per system | Startup stops with POLY-SYSTEM-003 |
| 3 | CommonModInitializer of every mod | main | Startup stops with POLY-ENTRYPOINT-003 |
| 4 | On a client, ClientModInitializer of every mod. On a dedicated server, DedicatedServerModInitializer | main | Same as above |
| 5 | Game layer starts | main | POLY-BOOTSTRAP-005 |
| 6 | Game main | main | — |
| 7 | Inside the game's start, after Minecraft's bootstrap: other loaders' mods initialise where their loader does it (Fabric: main, then client or server), then LifecycleEvents.MODS_INITIALIZED | main | A listener that throws is disabled (POLY-EVENT-001) |
- Steps 3 and 4 each go through all mods in dependency order. A mod that is depended on runs first. Ties are broken by mod ID. So the first client (or server) entrypoint runs only after every common entrypoint has finished.
- The integrated server in single-player runs inside the client process. In that case the
serverentrypoint is not called.serverruns only on a dedicated server. - When an entrypoint runs, the Minecraft registries and game objects do not exist yet. In an entrypoint, only declare.
- Mods of other loaders (Fabric) initialise later, in step 7, where their own loader runs them. A Poly Mod that needs another loader's mod to be initialised listens to
LifecycleEvents.MODS_INITIALIZED. It comes once, after every mod, and at the same point when no other loader's mod is installed. See Compatibility.
context.events().register(LifecycleEvents.MODS_INITIALIZED, () -> {
// Every mod, of every loader, has run its initialisers.
});
Entrypoint kinds and poly.mod.json keys are in Entrypoint.
Game layer and registration close
The game layer (poly-game-v26_2) starts after all entrypoints finish and before game main. If it cannot start, startup stops with POLY-BOOTSTRAP-005.
Registration closes just before the game freezes the registries.
- Poly Mod Loader validates each declaration in an entrypoint on the spot. A wrong or duplicate name throws immediately.
- Just before the game freezes the registries, the game layer turns all declarations into real game objects.
- Right after that, registration closes. Every later declaration throws
IllegalStateExceptionwith the message<code> … must be declared in a mod entrypoint; registration is closed.
Always finish declaring inside an entrypoint. Do not declare from an event listener, a scheduled task, or another thread.
Thread per callback
| Callback | Thread | What to keep in mind |
|---|---|---|
CommonModInitializer, ClientModInitializer, DedicatedServerModInitializer | main | Only declare |
SystemProvider.create | main, once before the entrypoints | Needs a public no-argument constructor |
GameBrandSystem.brand | any thread, many times | Keep it stateless and fast |
EventRegistry.register | any thread, any time | Thread-safe |
Server events (ServerEvents, PlayerEvents, BlockEvents, EntityEvents, WorldEvents, InteractionEvents) | server | ServerEvents.TICK fires every tick, so keep it fast |
ClientEvents | client | Fired only on the client. ITEM_TOOLTIP fires every frame a tooltip is shown, so keep it fast |
BlockBehavior, ItemBehavior | that world's thread, both sides | Check world.isClientSide() and change state only on the server |
BlockEntityTicker, AiGoal, ProjectileBehavior | server | Use a view you receive only during the call |
CommandAction, PermissionProvider | server | Permission checks are also called often when suggesting commands, so keep them fast |
Task from WorldView.schedule | server, after the given ticks | — |
ServerPayloadHandler / ClientPayloadHandler | server / client | — |
PayloadCodec | network thread | Do not access game state |
ConfigurationExchange | server part on the server, client reply on the client | Set the server part in the common entrypoint and the client reply in the client entrypoint |
HudLayer, BlockEntityPainter, WorldPainter | client (render), every frame | Keep it fast. Block entity drawing reads the client copy |
All methods of ModScreen, screen buttons | client (render) | — |
CodeFeature, StructureBuilder | world generation threads, several chunks at once | Keep state in local variables only. Use only FeatureContext.random() for randomness in placement features |
NoiseFunction | world generation threads, very often and concurrently | Stateless, fast, and always the same value at the same position |
TerrainGenerator | world generation threads, several columns at once | Thread-safe, and the same column for the same position and seed |
onDataReload, onDataFiles | server | At server start and on /reload |
onAssetReload, onAssetFiles | client | At client start and on F3+T. Not called on a dedicated server |
Task from onShutdown | Poly Mod Loader shutdown thread (Poly Mod Loader Shutdown) | See Shutdown below |
Views such as WorldView, EntityView, and PlayerView are valid only on the thread that passed them (server or client), and only while the callback runs. Do not store them in fields or hand them to another thread. In world generation callbacks, use only the context you receive instead of views.
If a callback throws
| Callback | Result |
|---|---|
| Entrypoint | POLY-ENTRYPOINT-003. Startup stops |
SystemProvider, system validation | POLY-SYSTEM-003. Startup stops |
| Event listener | POLY-EVENT-001. Only that listener is turned off. Other listeners and the game continue. A decision listener that throws or returns null (POLY-EVENT-003) counts as PASS |
HudLayer, BlockEntityPainter, WorldPainter | POLY-CLIENT-003. Only that layer or drawing is turned off |
CodeFeature, NoiseFunction, StructureBuilder | POLY-WORLDGEN-005. Logged once, and the failed spot is skipped |
Task from WorldView.schedule | POLY-SCHEDULE-001. Logged, and execution continues |
PayloadCodec.decode | POLY-NETWORK-004. That payload is dropped |
Mod code in ConfigurationExchange | POLY-NETWORK-005 |
BlockBehavior, ItemBehavior | Passed on to the game, like an exception from a game block |
| Shutdown task | POLY-SHUTDOWN-001. Logged, and the remaining tasks still run |
Registering too late, or on the wrong side
Declaring after registration has closed (for example inside an event listener) throws IllegalStateException with the area's code.
| Registration | Error after close |
|---|---|
| Blocks, items, entities, projectiles, effects, sounds, particles, game rules, item components, entity data, advancement triggers | POLY-CONTENT-002 |
| Commands | POLY-COMMAND-002 |
| Payload channels, configuration exchange | POLY-NETWORK-002 |
| Placement features, code placement features, density functions, structures, biomes, terrain generators | POLY-WORLDGEN-002 |
| Permission nodes, permission providers | POLY-PERMISSION-002 |
| Reload listeners | POLY-RESOURCE-002 |
| Game tests | POLY-GAMETEST-002 |
| Key bindings, HUD layers, block entity painters, dynamic textures, world painters, shader pipelines | POLY-CLIENT-002 |
onShutdown (after shutdown has started) | POLY-SHUTDOWN-002 |
If you pick the wrong side:
| Situation | Result |
|---|---|
Calling mod.client() on a dedicated server | IllegalStateException, POLY-CLIENT-001. Move client registration to the client entrypoint |
| Sending a payload before connecting | IllegalStateException, POLY-NETWORK-002 |
Calling sendToServer on a dedicated server, or sendToPlayer on a target that is not a server-side player | IllegalStateException |
| Calling a world-changing method on the client | IllegalStateException |
| Uploading a dynamic texture from a non-client thread | POLY-CLIENT-005 |
| Declaring content only in the client entrypoint | There is no separate error code, but the content list differs from the server and sync breaks. Declare content in the common entrypoint |
Shutdown
mod.onShutdown("close database", () -> database.close());
- Each task runs once on the Poly Mod Loader shutdown thread (
Poly Mod Loader Shutdown) when the JVM exits. - Tasks of all mods run in reverse registration order. So a mod that depends on another is cleaned up before the mod it depends on.
- Keep tasks short, do not start new threads, and do not touch game state. The game state may already be gone.
- If a task throws, Poly Mod Loader logs it as
POLY-SHUTDOWN-001with the mod ID and keeps running the remaining tasks. - Calling
onShutdownafter shutdown has started fails withPOLY-SHUTDOWN-002.
Work to do at the moment the server stops belongs in server events on the server thread, not in a shutdown task: ServerEvents.STOPPING before the world is saved, WorldEvents.UNLOADED for each dimension after it was saved, and ServerEvents.STOPPED after the worlds were closed.
See also
- Common rules — declaration timing, both sides matching, view lifetime
- System replacement — when
SystemProviderruns - Operations — config folder, shutdown tasks
- Error codes