---
title: TaskRegistry
type: docs
weight: 1
---

The TaskRegistry allows plugins to register and track asynchronous tasks. Hytale uses Java's standard concurrency APIs for task scheduling.

## Understanding TaskRegistry

{{< callout type="warning" >}}
**Important:** TaskRegistry does NOT have `runAsync()`, `runLater()`, or `runRepeating()` methods. These are common misconceptions from other platforms. Hytale uses Java's standard `CompletableFuture` and `ScheduledExecutorService` APIs.
{{< /callout >}}

## TaskRegistry API

```java
public class TaskRegistry extends Registry<TaskRegistration> {
    // Register a CompletableFuture task
    public TaskRegistration registerTask(CompletableFuture<Void> task);

    // Register a ScheduledFuture task
    public TaskRegistration registerTask(ScheduledFuture<Void> task);
}
```

The TaskRegistry tracks tasks for proper cleanup during plugin shutdown.

## Asynchronous Tasks

Use `CompletableFuture` for async operations:

```java
// Run async operation
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
    // Heavy computation, I/O, network requests
    String data = fetchFromDatabase(playerId);
});

// Register with TaskRegistry for tracking
getTaskRegistry().registerTask(task);

// Handle completion
task.thenAccept(result -> {
    // Process result - NOTE: This runs on a thread pool, not the world thread
    // Use world.execute() to run code on the world thread
});
```

### Returning to World Thread

After async operations, use `world.execute()` to return to the world's thread:

```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();

CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
    // Load from database (blocking I/O is OK here)
    PlayerData data = database.load(playerRef.getUuid());

    // Return to world thread for game state changes
    world.execute(() -> {
        Ref<EntityStore> ref = playerRef.getReference();
        if (ref != null) {
            Player p = ref.getStore().getComponent(ref, Player.getComponentType());
            if (p != null) {
                applyData(p, data);
                playerRef.sendMessage(Message.raw("Data loaded!"));
            }
        }
    });
});

getTaskRegistry().registerTask(task);
```

## Delayed Tasks

Use `CompletableFuture.delayedExecutor()` for delays:

```java
import java.util.concurrent.TimeUnit;

// Run after 3 seconds
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
    .execute(() -> {
        world.execute(() -> {
            playerRef.sendMessage(Message.raw("3 seconds have passed!"));
        });
    });
```

## Repeating Tasks

Use `ScheduledExecutorService` for repeating tasks:

```java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MyPlugin extends JavaPlugin {
    private ScheduledExecutorService scheduler;

    @Override
    public void start() {
        scheduler = Executors.newSingleThreadScheduledExecutor();

        // Run every 5 minutes
        ScheduledFuture<?> saveTask = scheduler.scheduleAtFixedRate(
            () -> {
                saveAllData();
                getLogger().at(Level.INFO).log("Auto-save complete");
            },
            5, // Initial delay
            5, // Period
            TimeUnit.MINUTES
        );

        // Register for tracking
        getTaskRegistry().registerTask((ScheduledFuture<Void>) saveTask);
    }

    @Override
    public void shutdown() {
        if (scheduler != null) {
            scheduler.shutdown();
        }
    }
}
```

## Tick-Based Timing

For tick-based timing, track ticks manually in a world tick handler:

```java
public class TickTimerPlugin extends JavaPlugin {
    private final Map<String, TickTimer> timers = new ConcurrentHashMap<>();

    public void scheduleAfterTicks(String id, int ticks, Runnable action) {
        timers.put(id, new TickTimer(ticks, action));
    }

    // Called from your tick handler
    public void onWorldTick() {
        timers.entrySet().removeIf(entry -> {
            TickTimer timer = entry.getValue();
            timer.ticksRemaining--;
            if (timer.ticksRemaining <= 0) {
                timer.action.run();
                return true; // Remove
            }
            return false;
        });
    }

    private static class TickTimer {
        int ticksRemaining;
        Runnable action;

        TickTimer(int ticks, Runnable action) {
            this.ticksRemaining = ticks;
            this.action = action;
        }
    }
}
```

## Common Patterns

### Countdown Timer

```java
public void startCountdown(PlayerRef playerRef, int seconds) {
    AtomicInteger remaining = new AtomicInteger(seconds);
    World world = Universe.get().getWorld(playerRef.getWorldUuid());

    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture<?> countdown = scheduler.scheduleAtFixedRate(() -> {
        int count = remaining.decrementAndGet();

        world.execute(() -> {
            if (count > 0) {
                playerRef.sendMessage(Message.raw("Starting in: " + count));
            } else {
                playerRef.sendMessage(Message.raw("Go!"));
                scheduler.shutdown();
            }
        });
    }, 0, 1, TimeUnit.SECONDS);
}
```

### Async Data Loading

```java
public void loadPlayerData(PlayerRef playerRef, World world) {
    CompletableFuture.runAsync(() -> {
        // Load from database (blocking I/O is OK here)
        PlayerData data = database.load(playerRef.getUuid());

        // Return to world thread
        world.execute(() -> {
            Ref<EntityStore> ref = playerRef.getReference();
            if (ref != null) {
                applyData(ref, data);
                playerRef.sendMessage(Message.raw("Data loaded!"));
            }
        });
    });
}
```

## Time Conversion

Hytale runs at **30 TPS** (ticks per second):

| Time | Ticks (at 30 TPS) | Milliseconds |
|------|-------------------|--------------|
| 1 tick | 1 | ~33ms |
| 1 second | 30 | 1,000ms |
| 5 seconds | 150 | 5,000ms |
| 1 minute | 1,800 | 60,000ms |
| 5 minutes | 9,000 | 300,000ms |

## Best Practices

{{< callout type="info" >}}
**Task Guidelines:**
- Use `CompletableFuture.runAsync()` for I/O, database, and network operations
- Use `world.execute()` to return to the world thread for game state changes
- Use `PlayerRef` in async tasks, not `Player` or `Ref<EntityStore>`
- Register long-running tasks with `getTaskRegistry()` for cleanup
- Shut down custom `ScheduledExecutorService` instances in `shutdown()`
{{< /callout >}}

{{< callout type="warning" >}}
**Thread Safety:** Each World runs on its own thread. Always use `world.execute()` or check `world.isInThread()` before modifying game state from async code.
{{< /callout >}}

```java
// Good: Proper async pattern with PlayerRef
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();

CompletableFuture.runAsync(() -> {
    String result = heavyComputation();

    world.execute(() -> {
        ref.sendMessage(Message.raw(result));
    });
});

// Bad: Using Player directly in async
// CompletableFuture.runAsync(() -> {
//     player.sendMessage("Not safe!");  // DON'T DO THIS
// });
```
