Apollo
Developers
Lightweight
JSON
Roundtrip Packets

Roundtrip Packets

Overview

This example demonstrates how to handle roundtrip packets between the server and the Lunar Client using JSON messages. 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.

Note that this method uses a different plugin channel for sending and receiving packets, which is apollo:json.

Integration

public class ApolloRoundtripJsonListener implements PluginMessageListener {
 
    private static final String TYPE_PREFIX = "type.googleapis.com/";
    private static final JsonParser JSON_PARSER = new JsonParser();
 
    @Getter
    private static ApolloRoundtripJsonListener instance;
 
    private final Map<UUID, Map<UUID, CompletableFuture<JsonObject>>> roundTripPacketFutures = new ConcurrentHashMap<>();
    private final Map<UUID, CompletableFuture<List<JsonObject>>> paginatedFutures = new ConcurrentHashMap<>();
    private final Map<UUID, List<JsonObject>> paginatedAccumulator = new ConcurrentHashMap<>();
    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
 
    public ApolloRoundtripJsonListener(ApolloExamplePlugin plugin) {
        instance = this;
        Bukkit.getServer().getMessenger().registerIncomingPluginChannel(plugin, "apollo:json", this);
    }
 
    @Override
    public void onPluginMessageReceived(@NonNull String channel, @NonNull Player player, byte[] bytes) {
        JsonObject payload;
        try {
            payload = JSON_PARSER.parse(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject();
        } catch (Exception e) {
            return;
        }
 
        if (!payload.has("@type")) {
            return;
        }
 
        String type = payload.get("@type").getAsString();
        if (type.startsWith(TYPE_PREFIX)) {
            type = type.substring(TYPE_PREFIX.length());
        }
 
        if ("lunarclient.apollo.transfer.v1.PingResponse".equals(type)
                || "lunarclient.apollo.transfer.v1.TransferResponse".equals(type)) {
            UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
            this.handleResponse(player, requestId, payload);
        } else if ("lunarclient.apollo.modsetting.v1.InstalledModsResponse".equals(type)) {
            UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
            this.parseModGroups(requestId, payload);
        }
    }
 
    public CompletableFuture<JsonObject> sendRequest(Player player, UUID requestId, JsonObject request, String requestType) {
        request.addProperty("@type", TYPE_PREFIX + requestType);
        request.addProperty("request_id", requestId.toString());
        JsonPacketUtil.sendPacket(player, request);
 
        CompletableFuture<JsonObject> future = new CompletableFuture<>();
 
        this.roundTripPacketFutures
            .computeIfAbsent(player.getUniqueId(), k -> new ConcurrentHashMap<>())
            .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));
        return future;
    }
 
    public CompletableFuture<List<JsonObject>> sendPaginatedRequest(Player player, UUID requestId, JsonObject request, String requestType) {
        request.addProperty("@type", TYPE_PREFIX + requestType);
        request.addProperty("request_id", requestId.toString());
        JsonPacketUtil.sendPacket(player, request);
 
        CompletableFuture<List<JsonObject>> 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 parseModGroups(UUID requestId, JsonObject response) {
        List<JsonObject> accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
 
        JsonArray modGroups = response.getAsJsonArray("mod_groups");
        if (modGroups != null) {
            for (JsonElement groupElement : modGroups) {
                JsonObject group = groupElement.getAsJsonObject();
                String modType = group.get("type").getAsString();
                JsonArray groupMods = group.getAsJsonArray("mods");
 
                if (groupMods != null) {
                    for (JsonElement modElement : groupMods) {
                        JsonObject mod = modElement.getAsJsonObject();
                        mod.addProperty("type", modType);
                        accumulated.add(mod);
                    }
                }
            }
        }
 
        this.handlePage(requestId, accumulated, response);
    }
 
    private void handlePage(UUID requestId, List<JsonObject> accumulated, JsonObject response) {
        int page = response.has("page") ? response.get("page").getAsInt() : 0;
        int totalPages = response.has("total_pages") ? response.get("total_pages").getAsInt() : 1;
 
        if (page == totalPages - 1) {
            this.paginatedAccumulator.remove(requestId);
 
            CompletableFuture<List<JsonObject>> future = this.paginatedFutures.remove(requestId);
            if (future != null) {
                future.complete(accumulated);
            }
        }
    }
 
    private void handleResponse(Player player, UUID requestId, JsonObject message) {
        Map<UUID, CompletableFuture<JsonObject>> futures = this.roundTripPacketFutures.get(player.getUniqueId());
        if (futures == null) {
            return;
        }
 
        CompletableFuture<JsonObject> future = futures.remove(requestId);
        if (future != null) {
            future.complete(message);
        }
    }
}