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

8.1 KiB

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

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

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

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

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

Common Patterns

Database Operations

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

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

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

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:

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