critical8 min

Threading Model

The most important concept in Hytale modding. Entity modifications MUST run on the World thread or your plugin will crash.

read this first
This is the #1 cause of plugin crashes. Any code that modifies entities, components, or world state MUST execute on the World thread. Violating this will cause silent failures or hard crashes.

What We Know

Entity modifications require world.execute() to be thread-safe. Events fire on various threads (network, etc.), not necessarily the thread that owns entity data.

The exact threading architecture isn't officially documented. What we know from observation:

  • world.execute() queues code to run on the world thread
  • world.isInThread() checks if you're already on the world thread
  • Direct entity modification from event handlers can crash
  • Packet sending is thread-safe (Netty handles it)
  • Read operations are generally safe
  • Commands run on ForkJoinPool.commonPool(), not the world thread

The Problem

Events fire on different threads. If you modify entity data directly from an event handler, you risk race conditions and crashes.

This Will Crash

1// WRONG - Direct modification from event thread
2getEventRegistry().register(PlayerConnectEvent.class, event -> {
3 Player player = event.getPlayer();
4
5 // CRASH! Event runs on network thread, not World thread
6 player.setModel("Mouse");
7});

The Solution: world.execute()

Always wrap entity modifications in world.execute():

1// CORRECT - Execute on World thread
2getEventRegistry().register(PlayerConnectEvent.class, event -> {
3 Player player = event.getPlayer();
4 World world = player.getWorld();
5
6 // Safe! Runs on the World thread
7 world.execute(() -> {
8 player.setModel("Mouse");
9 player.setSpeed(1.5f);
10 });
11});
how it works
world.execute() queues your code to run on the next World tick. This guarantees safe, synchronized access to entity data.

When To Use world.execute()

operationneeds world.execute()?reason
Reading player positionNoRead-only is generally safe
Changing player modelYESModifies component data
Sending packetsNoConnection is thread-safe
Modifying componentsYESAll component writes
Spawning entitiesYESWorld state modification
Teleporting playersYESTransform modification
Logging/printingNoNo entity interaction
Inside command execute()YESCommands run on ForkJoinPool
commands need world.execute() too!
Command handlers run on ForkJoinPool.commonPool(), not the world thread. If your command modifies entities, wrap it in world.execute().

Common Patterns

Event Handler Pattern

1private void onPlayerConnect(PlayerConnectEvent event) {
2 Player player = event.getPlayer();
3 World world = player.getWorld();
4
5 // Safe: packet sending doesn't need execute()
6 showWelcomeTitle(player);
7
8 // Unsafe operations go in execute()
9 world.execute(() -> {
10 setupPlayerComponents(player);
11 giveStarterItems(player);
12 });
13}
14
15private void showWelcomeTitle(Player player) {
16 // Packets are thread-safe
17 player.getPlayerConnection().write(
18 new ShowEventTitle(...)
19 );
20}
21
22private void setupPlayerComponents(Player player) {
23 // Component modifications - must be in execute()
24 Ref ref = player.getReference();
25 // ... modify components
26}

Delayed Execution

1// Execute after a delay (still on World thread)
2world.execute(() -> {
3 // First action
4 player.setModel("Ghost");
5
6 // Schedule another action
7 world.schedule(20, () -> { // 20 ticks = 1 second
8 player.resetModel();
9 });
10});

Batch Operations

1// Multiple players - batch in single execute
2World world = getUniverse().getWorlds().get(0);
3
4world.execute(() -> {
5 for (Player player : world.getPlayers()) {
6 player.setSpeed(2.0f);
7 player.sendMessage("Speed boost activated!");
8 }
9});

Debugging Thread Issues

If you see these errors, you likely have a threading issue:

1// Common error messages
2java.lang.IllegalStateException: Not on world thread
3java.util.ConcurrentModificationException
4NullPointerException in component access (silent failure)
silent failures
Some threading violations don't crash - they just silently fail. If your code "doesn't work" but produces no errors, check your threading!

Quick Reference

ThreadSafePlugin.javajava
1// Template for thread-safe event handling
2public class ThreadSafePlugin extends JavaPlugin {
3
4 @Override
5 protected void start() {
6 getEventRegistry().register(
7 PlayerConnectEvent.class,
8 this::handlePlayerConnect
9 );
10 }
11
12 private void handlePlayerConnect(PlayerConnectEvent event) {
13 Player player = event.getPlayer();
14
15 // Step 1: Read-only operations (safe anywhere)
16 String name = player.getName();
17 UUID uuid = player.getUuid();
18
19 // Step 2: Packet operations (safe anywhere)
20 sendWelcome(player);
21
22 // Step 3: Entity modifications (MUST use execute)
23 player.getWorld().execute(() -> {
24 modifyPlayer(player);
25 });
26 }
27
28 private void sendWelcome(Player player) {
29 // Thread-safe: packet sending
30 player.getPlayerConnection().write(...);
31 }
32
33 private void modifyPlayer(Player player) {
34 // Only call from world.execute()!
35 player.setModel("Mouse");
36 player.setSpeed(1.5f);
37 }
38}