5.4 KiB
title, type, weight
| title | type | weight |
|---|---|---|
| Threading | docs | 4 |
Understanding threading in Hytale is crucial for writing safe and performant plugins. Unlike many game servers, Hytale uses a per-world threading model where each world runs on its own dedicated thread.
Per-World Threading
Each World in Hytale extends TickingThread and runs on its own dedicated thread at 30 TPS (ticks per second). This means:
- Each world ticks independently
- World operations must be called from that world's thread
- Players in different worlds run on different threads
World world = player.getWorld();
// Check if we're on this world's thread
if (world.isInThread()) {
// Safe to modify world state
world.setBlock(position, blockType);
}
{{< callout type="warning" >}} Never perform blocking operations (I/O, network, database) on a world thread. This will cause that world to lag. {{< /callout >}}
Async Operations
For long-running operations, use CompletableFuture:
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.supplyAsync(() -> {
// This runs on a worker thread
return loadDataFromDatabase(ref.getUuid());
}).thenAccept(data -> {
// PlayerRef.sendMessage() is thread-safe
ref.sendMessage(Message.raw("Data loaded: " + data));
});
Thread-Safe Classes
Some Hytale classes are designed for thread safety:
| Class | Thread Safety |
|---|---|
PlayerRef |
Safe to use across threads |
World |
Must be accessed on its own thread (world.isInThread()) |
Entity |
Must be accessed on its world's thread |
ItemStack |
Immutable, thread-safe |
PlayerRef
PlayerRef is a persistent, thread-safe reference to a player:
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.Message;
// Store reference (thread-safe)
PlayerRef playerRef = player.getPlayerRef();
// Safe to access on any thread:
UUID uuid = playerRef.getUuid();
String username = playerRef.getUsername();
String language = playerRef.getLanguage();
// Get current world UUID (may change if player switches worlds)
UUID worldUuid = playerRef.getWorldUuid();
// Send message directly (thread-safe)
playerRef.sendMessage(Message.raw("Hello!"));
// For ECS operations, get the entity reference
Ref<EntityStore> entityRef = playerRef.getReference(); // null if not in world
Checking Thread Context
World world = player.getWorld();
// Check if on world's thread
if (!world.isInThread()) {
throw new IllegalStateException("Must be called from world thread!");
}
// Debug: print current thread
System.out.println("Current thread: " + Thread.currentThread().getName());
Common Patterns
Database Operations
public void savePlayer(Player player) {
PlayerRef ref = player.getPlayerRef();
PlayerData data = collectPlayerData(player);
CompletableFuture.runAsync(() -> {
// Runs on worker thread
database.save(ref.getUuid(), data);
}).thenRun(() -> {
// Notify player (PlayerRef.sendMessage is thread-safe)
ref.sendMessage(Message.raw("Saved!"));
});
}
Loading Data on Join
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef ref = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.supplyAsync(() -> {
// Load data on worker thread
return database.load(ref.getUuid());
}).thenAccept(data -> {
if (world != null && data != null) {
world.execute(() -> {
// Back on world thread - safe to access ECS
Ref<EntityStore> entityRef = ref.getReference();
if (entityRef != null) {
applyData(entityRef, data);
}
});
}
});
});
Async Events
Some events support asynchronous handling. Events with a keyed type (like PlayerChatEvent which has a String key) must use registerAsyncGlobal:
getEventRegistry().registerAsyncGlobal(
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// This runs asynchronously
// Can perform slow operations here
String filtered = filterMessage(event.getContent());
event.setContent(filtered);
return event;
})
);
Cross-World Operations
When working with entities across worlds or transferring players:
// Player transfer returns CompletableFuture
PlayerRef ref = player.getPlayerRef();
World targetWorld = universe.getWorld("target_world");
// Note: player operations that change worlds use async patterns
// Always use PlayerRef to track player across world changes
Best Practices
{{< callout type="tip" >}}
- Always use
PlayerRefwhen passing player references across async boundaries - Check
world.isInThread()before modifying world state - Use
CompletableFuturefor async database/network operations - Keep world thread operations fast (< 33ms per tick)
- Use connection pools for database access
- Remember: each world has its own thread - no single "main thread" {{< /callout >}}
Debugging Thread Issues
If you encounter threading issues:
- Check
world.isInThread()before world modifications - Use
PlayerRefinstead of directPlayerreferences for async work - Log the current thread:
Thread.currentThread().getName() - World thread names follow pattern:
World-{worldName}