Init
This commit is contained in:
69
content/core-concepts/tasks/_index.en.md
Normal file
69
content/core-concepts/tasks/_index.en.md
Normal 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));
|
||||
});
|
||||
});
|
||||
```
|
||||
69
content/core-concepts/tasks/_index.fr.md
Normal file
69
content/core-concepts/tasks/_index.fr.md
Normal 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));
|
||||
});
|
||||
});
|
||||
```
|
||||
295
content/core-concepts/tasks/async-operations.en.md
Normal file
295
content/core-concepts/tasks/async-operations.en.md
Normal 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 >}}
|
||||
295
content/core-concepts/tasks/async-operations.fr.md
Normal file
295
content/core-concepts/tasks/async-operations.fr.md
Normal 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 >}}
|
||||
260
content/core-concepts/tasks/task-registry.en.md
Normal file
260
content/core-concepts/tasks/task-registry.en.md
Normal 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
|
||||
// });
|
||||
```
|
||||
260
content/core-concepts/tasks/task-registry.fr.md
Normal file
260
content/core-concepts/tasks/task-registry.fr.md
Normal 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
|
||||
// });
|
||||
```
|
||||
Reference in New Issue
Block a user