Threading
Hytale uses a multi-threaded architecture where each world runs on its own thread. Understanding this model is essential for writing correct mod code.
Threading Model Overview
Section titled “Threading Model Overview”| Thread | Purpose |
|---|---|
| Main Thread | Server bootstrap and shutdown orchestration |
| WorldThread - {name} | Per-world tick loop (entities, chunks, tasks) |
| ChunkLighting - {name} | Per-world light propagation |
| WorldMap - {name} | Per-world map generation |
| ForkJoinPool.common | Async I/O, chunk loading |
| Scheduler | Periodic tasks (single-threaded) |
There is no single “main game thread” - each world is independent.
World Threads
Section titled “World Threads”Each World extends TickingThread and runs at 30 TPS by default. The tick loop:
- Process queued tasks (
taskQueue) - Tick all entities (
entityStore.tick()) - Tick all chunks (
chunkStore.tick()) - Process any new tasks
- Sleep until next tick
// world tick cycle (simplified)protected void tick(float dt) { this.consumeTaskQueue(); // run scheduled work this.entityStore.tick(dt); // tick entities this.chunkStore.tick(dt); // tick chunks this.consumeTaskQueue(); // run any new work this.tick++;}Thread Safety Rules
Section titled “Thread Safety Rules”Operations that require the world thread:
| Operation | Why |
|---|---|
Block access (setBlock, getState) | Modifies chunk data |
Entity manipulation (addEntity, removeEntity) | Modifies entity store |
Entity lookup (world.getEntity(uuid)) | Iterates entity store |
Chunk access (getChunkIfLoaded) | Accesses chunk store |
Component modification (store.addComponent) | Not thread-safe |
Operations safe from any thread:
| Operation | Why |
|---|---|
Universe.get() | Static singleton |
Universe.get().getWorlds() | ConcurrentHashMap |
Universe.get().getPlayers() | Returns snapshot copy |
playerRef.getUuid(), getUsername() | Immutable fields |
world.getName() | Immutable field |
world.isAlive() | AtomicBoolean |
world.getPlayerRefs() | Unmodifiable concurrent view |
world.getPlayerCount() | Concurrent map size |
Cross-Thread Patterns
Section titled “Cross-Thread Patterns”world.execute()
Section titled “world.execute()”Schedule work to run on the world thread:
// from an event handler (may run on any thread)private void onPlayerDisconnect(PlayerDisconnectEvent event) { PlayerRef playerRef = event.getPlayerRef(); Ref<EntityStore> ref = playerRef.getReference(); if (ref == null) return;
World world = ref.getStore().getExternalData().getWorld(); world.execute(() -> { if (!ref.isValid()) return; store.removeComponent(ref, MyComponent.getComponentType()); });}isInThread() Check
Section titled “isInThread() Check”For methods that may be called from either context:
public void doSomething(World world, Ref<EntityStore> ref) { if (world.isInThread()) { // already on world thread - do work directly actuallyDoSomething(world, ref); } else { // wrong thread - schedule it world.execute(() -> actuallyDoSomething(world, ref)); }}debugAssertInTickingThread()
Section titled “debugAssertInTickingThread()”Assert that code runs on world thread (throws if not):
public void doWorldOnlyThing() { world.debugAssertInTickingThread(); // throws if wrong thread // ... implementation}Common Mistakes
Section titled “Common Mistakes”Stale Refs in Delayed Execution
Section titled “Stale Refs in Delayed Execution”A Ref<EntityStore> becomes invalid when the entity is removed (death, despawn, player disconnect, chunk unload). This matters when there’s a delay between capturing the ref and using it:
// delayed task - entity might be gone by execution timeHytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { world.execute(() -> { if (!ref.isValid()) return; Player player = store.getComponent(ref, Player.getComponentType()); // ... });}, 30, TimeUnit.SECONDS);For immediate world.execute() calls (same tick), the ref is almost always still valid. The check matters more for:
- Scheduled tasks with delays (seconds/minutes)
- Stored refs used later (cached in a field)
- Async operations that take time
Blocking the World Thread
Section titled “Blocking the World Thread”Blocking the world thread freezes all ticking:
world.execute(() -> { Thread.sleep(5000); // world freezes for 5 seconds!});Use the scheduled executor for delays instead:
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { world.execute(() -> { // do delayed work });}, 5, TimeUnit.SECONDS);Integration with Scheduling
Section titled “Integration with Scheduling”The Scheduling page covers HytaleServer.SCHEDULED_EXECUTOR for:
- Delayed tasks
- Repeating tasks
- Posting results back to world thread
Run periodic task, post results to world:
HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> { // runs on scheduler thread SomeData data = computeSomething();
// post result to world thread world.execute(() -> { applyData(data); });}, 1, 1, TimeUnit.MINUTES);