This commit is contained in:
2026-01-20 20:33:59 +01:00
commit b16a40e431
583 changed files with 87339 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
---
title: Tasks
type: docs
weight: 3
---
The task system in Hytale manages asynchronous operations using Java's standard concurrency APIs.
{{< cards >}}
{{< card link="task-registry" title="TaskRegistry" subtitle="Registering and tracking tasks" >}}
{{< card link="async-operations" title="Async Operations" subtitle="Thread-safe async patterns" >}}
{{< /cards >}}
## Overview
{{< callout type="warning" >}}
**Important:** TaskRegistry does NOT have `runAsync()`, `runSync()`, `runLater()`, or `runRepeating()` methods. Hytale uses Java's standard `CompletableFuture` and `ScheduledExecutorService` APIs.
{{< /callout >}}
The TaskRegistry allows plugins to:
- **Register tasks** - Track `CompletableFuture` and `ScheduledFuture` for cleanup
- **Async operations** - Use `CompletableFuture.runAsync()`
- **Delayed operations** - Use `CompletableFuture.delayedExecutor()`
- **Repeating operations** - Use `ScheduledExecutorService`
## Quick Start
```java
// Run async operation
CompletableFuture.runAsync(() -> {
// Background thread code
Data result = computeExpensiveData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
// Delayed operation (3 seconds)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
// Delayed code on world thread
});
});
```
## Key Concepts
{{< callout type="warning" >}}
**Thread Safety:** Each World runs on its own dedicated thread. Always use `world.execute()` when modifying game state (players, entities, blocks) from async code. Direct manipulation from async threads can cause crashes or data corruption.
{{< /callout >}}
```java
// Correct pattern
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Do heavy computation here (background thread)
Data result = computeExpensiveData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
```

View File

@@ -0,0 +1,69 @@
---
title: Tâches
type: docs
weight: 3
---
Le système de tâches dans Hytale gère les opérations asynchrones en utilisant les APIs de concurrence standard de Java.
{{< cards >}}
{{< card link="task-registry" title="TaskRegistry" subtitle="Enregistrement et suivi des tâches" >}}
{{< card link="async-operations" title="Opérations Async" subtitle="Patterns async thread-safe" >}}
{{< /cards >}}
## Aperçu
{{< callout type="warning" >}}
**Important :** TaskRegistry n'a PAS de méthodes `runAsync()`, `runSync()`, `runLater()`, ou `runRepeating()`. Hytale utilise les APIs standard Java `CompletableFuture` et `ScheduledExecutorService`.
{{< /callout >}}
Le TaskRegistry permet aux plugins de :
- **Enregistrer des tâches** - Suivre les `CompletableFuture` et `ScheduledFuture` pour le nettoyage
- **Opérations async** - Utiliser `CompletableFuture.runAsync()`
- **Opérations différées** - Utiliser `CompletableFuture.delayedExecutor()`
- **Opérations répétitives** - Utiliser `ScheduledExecutorService`
## Démarrage Rapide
```java
// Exécuter une opération async
CompletableFuture.runAsync(() -> {
// Code thread d'arrière-plan
Data result = computeExpensiveData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
// Opération différée (3 secondes)
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
// Code différé sur le thread du monde
});
});
```
## Concepts Clés
{{< callout type="warning" >}}
**Sécurité des Threads :** Chaque World s'exécute sur son propre thread dédié. Utilisez toujours `world.execute()` lors de la modification de l'état du jeu (joueurs, entités, blocs) depuis du code async. La manipulation directe depuis des threads async peut causer des crashs ou corruption de données.
{{< /callout >}}
```java
// Pattern correct
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Faire le calcul lourd ici (thread d'arrière-plan)
Data result = computeExpensiveData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
```

View File

@@ -0,0 +1,295 @@
---
title: Async Operations
type: docs
weight: 2
---
Asynchronous operations allow plugins to perform heavy tasks without blocking the world's ticking thread.
## Why Async?
{{< callout type="info" >}}
Each World has a dedicated ticking thread that handles game logic, player interactions, and tick updates. Blocking it causes lag and poor player experience. Use async for:
- Database queries
- File I/O
- Network requests
- Complex calculations
{{< /callout >}}
## Basic Async Pattern
Use `CompletableFuture.runAsync()` and `world.execute()`:
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// This runs on a background thread
// Do heavy work here
Data result = computeData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
```
## Thread Safety
### Using PlayerRef
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Load data asynchronously
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Return to world thread
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
```
### Thread-Safe Collections
```java
// Use concurrent collections for shared data
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Prevent duplicate processing
if (!processing.add(uuid)) {
return; // Already processing
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Processing complete!"));
});
} finally {
processing.remove(uuid);
}
});
}
```
## Async Events
Handle async events with CompletableFuture:
```java
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Async processing
return processEvent(event);
});
});
```
## Common Patterns
### Database Operations
```java
public void savePlayerData(PlayerRef playerRef, World world) {
// Capture data on world thread before going async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Saved data for " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Failed to save data", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
### HTTP Requests
```java
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Failed to fetch stats."));
});
}
});
}
```
### Batch Processing
```java
public void processAllPlayers(World world) {
// Capture player refs on world thread
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
```
## Error Handling
```java
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation successful!"));
});
} catch (Exception e) {
getLogger().error("Async operation failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed. Please try again."));
});
}
});
}
```
## Chaining Async Operations
Use `CompletableFuture` chaining for sequential async operations:
```java
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// First async operation
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Second async operation
return processData(data);
})
.thenAccept(result -> {
// Return to world thread with final result
world.execute(() -> {
playerRef.sendMessage(Message.raw("Final result: " + result));
});
})
.exceptionally(e -> {
getLogger().error("Chain failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed."));
});
return null;
});
}
```
## Best Practices
{{< callout type="warning" >}}
**Async Rules:**
1. Never access game state directly from async threads
2. Always use `PlayerRef`, never store `Player` or `Ref<EntityStore>`
3. Capture needed data before going async
4. Use `world.execute()` to return to world thread
5. Handle exceptions properly
6. Use concurrent collections for shared data
{{< /callout >}}
```java
// CORRECT: Capture data, process async, apply on world thread
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capture on world thread
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// WRONG: Using world objects directly in async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // DON'T DO THIS - these may be invalid from async context
// String name = player.getDisplayName();
// player.sendMessage("Hello");
// });
// }
```
## Comparison with Other Platforms
{{< callout type="info" >}}
**Note for developers from other platforms:**
- Unlike Bukkit/Spigot, Hytale does NOT have `runSync()` or `runLater()` methods
- Use `CompletableFuture.runAsync()` instead of platform-specific async methods
- Use `world.execute()` instead of `runSync()` to return to the world thread
- Use `CompletableFuture.delayedExecutor()` for delayed tasks
- Use `ScheduledExecutorService` for repeating tasks
{{< /callout >}}

View File

@@ -0,0 +1,295 @@
---
title: Opérations Async
type: docs
weight: 2
---
Les opérations asynchrones permettent aux plugins d'effectuer des tâches lourdes sans bloquer le thread de tick du monde.
## Pourquoi Async ?
{{< callout type="info" >}}
Chaque World a un thread de tick dédié qui gère la logique du jeu, les interactions des joueurs et les mises à jour de tick. Le bloquer cause du lag et une mauvaise expérience joueur. Utilisez async pour :
- Les requêtes de base de données
- Les I/O de fichiers
- Les requêtes réseau
- Les calculs complexes
{{< /callout >}}
## Pattern Async de Base
Utilisez `CompletableFuture.runAsync()` et `world.execute()` :
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Ceci s'exécute sur un thread d'arrière-plan
// Faire le travail lourd ici
Data result = computeData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
```
## Sécurité des Threads
### Utiliser PlayerRef
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Charger les données de manière asynchrone
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Retourner au thread du monde
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
```
### Collections Thread-Safe
```java
// Utiliser des collections concurrentes pour les données partagées
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Empêcher le traitement en double
if (!processing.add(uuid)) {
return; // Déjà en traitement
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Traitement terminé !"));
});
} finally {
processing.remove(uuid);
}
});
}
```
## Événements Async
Gérer les événements async avec CompletableFuture :
```java
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Traitement async
return processEvent(event);
});
});
```
## Patterns Courants
### Opérations de Base de Données
```java
public void savePlayerData(PlayerRef playerRef, World world) {
// Capturer les données sur le thread du monde avant de passer en async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Données sauvegardées pour " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Échec de la sauvegarde des données", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
});
});
}
```
### Requêtes HTTP
```java
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Échec de la récupération des stats."));
});
}
});
}
```
### Traitement par Lots
```java
public void processAllPlayers(World world) {
// Capturer les refs de joueurs sur le thread du monde
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
```
## Gestion des Erreurs
```java
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération réussie !"));
});
} catch (Exception e) {
getLogger().error("L'opération async a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée. Veuillez réessayer."));
});
}
});
}
```
## Chaînage des Opérations Async
Utilisez le chaînage `CompletableFuture` pour les opérations async séquentielles :
```java
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// Première opération async
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Deuxième opération async
return processData(data);
})
.thenAccept(result -> {
// Retourner au thread du monde avec le résultat final
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat final : " + result));
});
})
.exceptionally(e -> {
getLogger().error("La chaîne a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée."));
});
return null;
});
}
```
## Bonnes Pratiques
{{< callout type="warning" >}}
**Règles Async :**
1. Ne jamais accéder à l'état du jeu directement depuis des threads async
2. Toujours utiliser `PlayerRef`, ne jamais stocker `Player` ou `Ref<EntityStore>`
3. Capturer les données nécessaires avant de passer en async
4. Utiliser `world.execute()` pour retourner au thread du monde
5. Gérer les exceptions correctement
6. Utiliser des collections concurrentes pour les données partagées
{{< /callout >}}
```java
// CORRECT : Capturer les données, traiter en async, appliquer sur le thread du monde
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capturer sur le thread du monde
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// MAUVAIS : Utiliser les objets monde directement en async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // NE FAITES PAS ÇA - ceux-ci peuvent être invalides depuis le contexte async
// String name = player.getDisplayName();
// player.sendMessage("Bonjour");
// });
// }
```
## Comparaison avec d'Autres Plateformes
{{< callout type="info" >}}
**Note pour les développeurs d'autres plateformes :**
- Contrairement à Bukkit/Spigot, Hytale n'a PAS de méthodes `runSync()` ou `runLater()`
- Utilisez `CompletableFuture.runAsync()` au lieu des méthodes async spécifiques à la plateforme
- Utilisez `world.execute()` au lieu de `runSync()` pour retourner au thread du monde
- Utilisez `CompletableFuture.delayedExecutor()` pour les tâches différées
- Utilisez `ScheduledExecutorService` pour les tâches répétitives
{{< /callout >}}

View File

@@ -0,0 +1,260 @@
---
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
// });
```

View File

@@ -0,0 +1,260 @@
---
title: TaskRegistry
type: docs
weight: 1
---
Le TaskRegistry permet aux plugins d'enregistrer et de suivre les tâches asynchrones. Hytale utilise les APIs de concurrence standard de Java pour la planification des tâches.
## Comprendre TaskRegistry
{{< callout type="warning" >}}
**Important :** TaskRegistry n'a PAS de méthodes `runAsync()`, `runLater()`, ou `runRepeating()`. Ce sont des idées fausses courantes venant d'autres plateformes. Hytale utilise les APIs standard Java `CompletableFuture` et `ScheduledExecutorService`.
{{< /callout >}}
## API TaskRegistry
```java
public class TaskRegistry extends Registry<TaskRegistration> {
// Enregistrer une tâche CompletableFuture
public TaskRegistration registerTask(CompletableFuture<Void> task);
// Enregistrer une tâche ScheduledFuture
public TaskRegistration registerTask(ScheduledFuture<Void> task);
}
```
Le TaskRegistry suit les tâches pour un nettoyage correct lors de l'arrêt du plugin.
## Tâches Asynchrones
Utilisez `CompletableFuture` pour les opérations async :
```java
// Exécuter une opération async
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Calcul lourd, I/O, requêtes réseau
String data = fetchFromDatabase(playerId);
});
// Enregistrer avec TaskRegistry pour le suivi
getTaskRegistry().registerTask(task);
// Gérer l'achèvement
task.thenAccept(result -> {
// Traiter le résultat - NOTE : Ceci s'exécute sur un pool de threads, pas le thread du monde
// Utilisez world.execute() pour exécuter du code sur le thread du monde
});
```
### Retourner au Thread du Monde
Après les opérations async, utilisez `world.execute()` pour retourner au thread du monde :
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
// Charger depuis la base de données (I/O bloquant est OK ici)
PlayerData data = database.load(playerRef.getUuid());
// Retourner au thread du monde pour les changements d'état du jeu
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("Données chargées !"));
}
}
});
});
getTaskRegistry().registerTask(task);
```
## Tâches Différées
Utilisez `CompletableFuture.delayedExecutor()` pour les délais :
```java
import java.util.concurrent.TimeUnit;
// Exécuter après 3 secondes
CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS)
.execute(() -> {
world.execute(() -> {
playerRef.sendMessage(Message.raw("3 secondes se sont écoulées !"));
});
});
```
## Tâches Répétitives
Utilisez `ScheduledExecutorService` pour les tâches répétitives :
```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();
// Exécuter toutes les 5 minutes
ScheduledFuture<?> saveTask = scheduler.scheduleAtFixedRate(
() -> {
saveAllData();
getLogger().at(Level.INFO).log("Auto-sauvegarde terminée");
},
5, // Délai initial
5, // Période
TimeUnit.MINUTES
);
// Enregistrer pour le suivi
getTaskRegistry().registerTask((ScheduledFuture<Void>) saveTask);
}
@Override
public void shutdown() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
```
## Timing Basé sur les Ticks
Pour un timing basé sur les ticks, suivez les ticks manuellement dans un gestionnaire de tick du monde :
```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));
}
// Appeler depuis votre gestionnaire de tick
public void onWorldTick() {
timers.entrySet().removeIf(entry -> {
TickTimer timer = entry.getValue();
timer.ticksRemaining--;
if (timer.ticksRemaining <= 0) {
timer.action.run();
return true; // Supprimer
}
return false;
});
}
private static class TickTimer {
int ticksRemaining;
Runnable action;
TickTimer(int ticks, Runnable action) {
this.ticksRemaining = ticks;
this.action = action;
}
}
}
```
## Patterns Courants
### Compte à Rebours
```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("Démarrage dans : " + count));
} else {
playerRef.sendMessage(Message.raw("Go !"));
scheduler.shutdown();
}
});
}, 0, 1, TimeUnit.SECONDS);
}
```
### Chargement de Données Async
```java
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
// Charger depuis la base de données (I/O bloquant est OK ici)
PlayerData data = database.load(playerRef.getUuid());
// Retourner au thread du monde
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
});
});
}
```
## Conversion de Temps
Hytale tourne à **30 TPS** (ticks par seconde) :
| Temps | Ticks (à 30 TPS) | Millisecondes |
|-------|------------------|---------------|
| 1 tick | 1 | ~33ms |
| 1 seconde | 30 | 1 000ms |
| 5 secondes | 150 | 5 000ms |
| 1 minute | 1 800 | 60 000ms |
| 5 minutes | 9 000 | 300 000ms |
## Bonnes Pratiques
{{< callout type="info" >}}
**Directives pour les Tâches :**
- Utilisez `CompletableFuture.runAsync()` pour les opérations I/O, base de données et réseau
- Utilisez `world.execute()` pour retourner au thread du monde pour les changements d'état du jeu
- Utilisez `PlayerRef` dans les tâches async, pas `Player` ou `Ref<EntityStore>`
- Enregistrez les tâches longue durée avec `getTaskRegistry()` pour le nettoyage
- Arrêtez les instances `ScheduledExecutorService` personnalisées dans `shutdown()`
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety :** Chaque World s'exécute sur son propre thread. Utilisez toujours `world.execute()` ou vérifiez `world.isInThread()` avant de modifier l'état du jeu depuis du code async.
{{< /callout >}}
```java
// Bien : Pattern async correct avec PlayerRef
PlayerRef ref = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
String result = heavyComputation();
world.execute(() -> {
ref.sendMessage(Message.raw(result));
});
});
// Mauvais : Utiliser Player directement en async
// CompletableFuture.runAsync(() -> {
// player.sendMessage("Pas sûr !"); // NE FAITES PAS ÇA
// });
```