Skip to Content

Roundtrip Packets

Overview

This example demonstrates how to handle roundtrip packets between the server and the Lunar Client. These packets are sent from the server, expecting a corresponding response from the client. The example utilizes a map to track the requests and their corresponding responses. For instance, this pattern is common in modules like TransferModule where a request packet is sent and a response is awaited.

Integration

public class ApolloRoundtripProtoListener implements PluginMessageListener { @Getter private static ApolloRoundtripProtoListener instance; private final Map<UUID, Map<UUID, CompletableFuture<GeneratedMessageV3>>> roundTripPacketFutures = new ConcurrentHashMap<>(); private final Map<UUID, CompletableFuture<List<Mod>>> paginatedFutures = new ConcurrentHashMap<>(); private final Map<UUID, List<Mod>> paginatedAccumulator = new ConcurrentHashMap<>(); private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) { instance = this; Bukkit.getServer().getMessenger().registerIncomingPluginChannel(plugin, "lunar:apollo", this); } @Override public void onPluginMessageReceived(@NonNull String channel, @NonNull Player player, byte[] bytes) { try { Any any = Any.parseFrom(bytes); if (any.is(PingResponse.class)) { PingResponse message = any.unpack(PingResponse.class); UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8()); this.handleResponse(player, requestId, message); } else if (any.is(TransferResponse.class)) { TransferResponse message = any.unpack(TransferResponse.class); UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8()); this.handleResponse(player, requestId, message); } else if (any.is(InstalledModsResponse.class)) { InstalledModsResponse message = any.unpack(InstalledModsResponse.class); UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8()); this.handlePagedResponse(requestId, message); } } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } } public <T extends GeneratedMessageV3> CompletableFuture<T> sendRequest(Player player, UUID requestId, GeneratedMessageV3 request, Class<T> responseType) { ProtobufPacketUtil.sendPacket(player, request); CompletableFuture<T> future = new CompletableFuture<>(); this.roundTripPacketFutures .computeIfAbsent(player.getUniqueId(), k -> new ConcurrentHashMap<>()) .put(requestId, (CompletableFuture<GeneratedMessageV3>) future); ScheduledFuture<?> timeoutTask = this.executorService.schedule(() -> future.completeExceptionally(new TimeoutException("Response timed out")), 10, TimeUnit.SECONDS ); future.whenComplete((result, throwable) -> timeoutTask.cancel(false)); return future; } public CompletableFuture<List<Mod>> sendPaginatedRequest(Player player, UUID requestId, GeneratedMessageV3 request) { ProtobufPacketUtil.sendPacket(player, request); CompletableFuture<List<Mod>> future = new CompletableFuture<>(); this.paginatedFutures.put(requestId, future); ScheduledFuture<?> timeoutTask = this.executorService.schedule(() -> future.completeExceptionally(new TimeoutException("Response timed out")), 10, TimeUnit.SECONDS ); future.whenComplete((result, throwable) -> { timeoutTask.cancel(false); this.paginatedAccumulator.remove(requestId); }); return future; } private void handlePagedResponse(UUID requestId, InstalledModsResponse response) { List<Mod> accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>()); response.getModGroupsList().forEach(group -> accumulated.addAll(group.getModsList()) ); if (response.getPage() == response.getTotalPages() - 1) { this.paginatedAccumulator.remove(requestId); CompletableFuture<List<Mod>> future = this.paginatedFutures.remove(requestId); if (future != null) { future.complete(accumulated); } } } private <T extends GeneratedMessageV3> void handleResponse(Player player, UUID requestId, T message) { Map<UUID, CompletableFuture<GeneratedMessageV3>> futures = this.roundTripPacketFutures.get(player.getUniqueId()); if (futures == null) { return; } CompletableFuture<GeneratedMessageV3> future = futures.remove(requestId); if (future != null) { future.complete(message); } } }

Here’s an example demonstrating how to use the code to handle a server-to-client TransferModule transfer, where the client responds with a status (accepted or rejected).

public void transferExample(Player player) { UUID requestId = UUID.randomUUID(); TransferRequest transferRequestMessage = TransferRequest.newBuilder() .setRequestId(ByteString.copyFromUtf8(requestId.toString())) .setServerIp("mc.hypixel.net") .build(); ApolloRoundtripProtoListener.getInstance() .sendRequest(player, requestId, transferRequestMessage, TransferResponse.class) .thenAccept(response -> { String message = ""; switch (response.getStatus()) { case STATUS_ACCEPTED: { message = "Transfer accepted! Goodbye!"; break; } case STATUS_REJECTED: { message = "Transfer rejected by client!"; break; } } player.sendMessage(message); }).exceptionally(throwable -> { player.sendMessage("Failed to receive a response in time."); return null; }); }