Apollo
Developers
Lightweight
Protobuf
Roundtrip Packets

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 ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
 
    public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) {
        instance = this;
        Bukkit.getServer().getMessenger().registerIncomingPluginChannel(plugin, "lunar:apollo", this);
    }
 
    @Override
    public void onPluginMessageReceived(String s, 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);
            }
 
        } 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;
    }
 
    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;
        });
}