Files
Documentation/content/core-concepts/threading.en.md
2026-01-20 20:33:59 +01:00

191 lines
5.4 KiB
Markdown

---
title: Threading
type: docs
weight: 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
```java
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`:
```java
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:
```java
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
```java
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
```java
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
```java
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`:
```java
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:
```java
// 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 `PlayerRef` when passing player references across async boundaries
- Check `world.isInThread()` before modifying world state
- Use `CompletableFuture` for 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:
1. Check `world.isInThread()` before world modifications
2. Use `PlayerRef` instead of direct `Player` references for async work
3. Log the current thread: `Thread.currentThread().getName()`
4. World thread names follow pattern: `World-{worldName}`