8.1 KiB
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:
- Never access game state directly from async threads
- Always use
PlayerRef, never storePlayerorRef<EntityStore> - Capture needed data before going async
- Use
world.execute()to return to world thread - Handle exceptions properly
- 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()orrunLater()methods - Use
CompletableFuture.runAsync()instead of platform-specific async methods - Use
world.execute()instead ofrunSync()to return to the world thread - Use
CompletableFuture.delayedExecutor()for delayed tasks - Use
ScheduledExecutorServicefor repeating tasks {{< /callout >}}