Files
Documentation/content/core-concepts/tasks/async-operations.fr.md
2026-01-20 20:33:59 +01:00

8.8 KiB

title, type, weight
title type weight
Opérations Async docs 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() :

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

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

// 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 :

getEventRegistry().registerAsync(AsyncEvent.class, event -> {
    return CompletableFuture.supplyAsync(() -> {
        // Traitement async
        return processEvent(event);
    });
});

Patterns Courants

Opérations de Base de Données

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

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

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

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 :

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