191 lines
5.4 KiB
Markdown
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}`
|