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

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 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}