diff --git a/api/src/main/java/com/lunarclient/apollo/client/mod/LunarClientMod.java b/api/src/main/java/com/lunarclient/apollo/client/mod/LunarClientMod.java index 28c02c6c..60e2f50f 100644 --- a/api/src/main/java/com/lunarclient/apollo/client/mod/LunarClientMod.java +++ b/api/src/main/java/com/lunarclient/apollo/client/mod/LunarClientMod.java @@ -50,7 +50,7 @@ public final class LunarClientMod { * @return the mod display name * @since 1.0.6 */ - String displayName; + @Nullable String displayName; /** * Returns the mod {@link String} version (e.g. '1.2.21'). diff --git a/api/src/main/java/com/lunarclient/apollo/event/player/ApolloPlayerHandshakeEvent.java b/api/src/main/java/com/lunarclient/apollo/event/player/ApolloPlayerHandshakeEvent.java index 418533c0..d1e6cb30 100644 --- a/api/src/main/java/com/lunarclient/apollo/event/player/ApolloPlayerHandshakeEvent.java +++ b/api/src/main/java/com/lunarclient/apollo/event/player/ApolloPlayerHandshakeEvent.java @@ -27,6 +27,7 @@ import com.lunarclient.apollo.client.version.LunarClientVersion; import com.lunarclient.apollo.client.version.MinecraftVersion; import com.lunarclient.apollo.event.Event; +import com.lunarclient.apollo.module.modsetting.ModSettingModule; import com.lunarclient.apollo.module.paynow.PayNowEmbeddedCheckoutSupport; import com.lunarclient.apollo.module.tebex.TebexEmbeddedCheckoutSupport; import com.lunarclient.apollo.player.ApolloPlayer; @@ -69,9 +70,10 @@ public class ApolloPlayerHandshakeEvent implements Event { * A {@link List} of {@link LunarClientMod} the player has installed. * * @return the installed mods + * @deprecated for removal since 1.2.5, use {@link ModSettingModule#requestInstalledMods(ApolloPlayer)} instead. * @since 1.0.6 */ - List installedMods; + @Deprecated List installedMods; /** * The {@link TebexEmbeddedCheckoutSupport} type. diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsRequest.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsRequest.java new file mode 100644 index 00000000..cbbcb56c --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsRequest.java @@ -0,0 +1,37 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.modsetting; + +import com.lunarclient.apollo.roundtrip.ApolloRequest; +import lombok.Builder; + +/** + * Represents the installed mods request. + * + * @since 1.2.5 + */ +@Builder +public final class InstalledModsRequest extends ApolloRequest { + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsResponse.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsResponse.java new file mode 100644 index 00000000..9ee8350b --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/InstalledModsResponse.java @@ -0,0 +1,50 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.modsetting; + +import com.lunarclient.apollo.client.mod.LunarClientMod; +import com.lunarclient.apollo.roundtrip.pagination.ApolloPaginatedResponse; +import java.util.List; +import java.util.UUID; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +/** + * Represents the installed mods response. + * + * @since 1.2.5 + */ +@Getter +@SuperBuilder +public final class InstalledModsResponse extends ApolloPaginatedResponse { + + @Override + public ApolloPaginatedResponse combine(UUID packetId, List elements) { + return InstalledModsResponse.builder() + .packetId(packetId) + .elements(elements) + .build(); + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModSettingModule.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModSettingModule.java index cdad6ef4..c166aed9 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModSettingModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModSettingModule.java @@ -24,6 +24,7 @@ package com.lunarclient.apollo.module.modsetting; import com.lunarclient.apollo.ApolloPlatform; +import com.lunarclient.apollo.async.Future; import com.lunarclient.apollo.module.ApolloModule; import com.lunarclient.apollo.module.ModuleDefinition; import com.lunarclient.apollo.option.Option; @@ -66,4 +67,13 @@ public boolean isClientNotify() { */ public abstract > T getStatus(@NotNull ApolloPlayer player, @NonNull C option); + /** + * Sends the {@link InstalledModsRequest} to the {@link ApolloPlayer}. + * + * @param player the player + * @return future to be listened to for errors/success + * @since 1.2.5 + */ + public abstract Future requestInstalledMods(ApolloPlayer player); + } diff --git a/api/src/main/java/com/lunarclient/apollo/player/ApolloPlayer.java b/api/src/main/java/com/lunarclient/apollo/player/ApolloPlayer.java index 3a2bc71f..d0b4c710 100644 --- a/api/src/main/java/com/lunarclient/apollo/player/ApolloPlayer.java +++ b/api/src/main/java/com/lunarclient/apollo/player/ApolloPlayer.java @@ -27,6 +27,7 @@ import com.lunarclient.apollo.client.version.LunarClientVersion; import com.lunarclient.apollo.client.version.MinecraftVersion; import com.lunarclient.apollo.common.location.ApolloLocation; +import com.lunarclient.apollo.module.modsetting.ModSettingModule; import com.lunarclient.apollo.module.paynow.PayNowEmbeddedCheckoutSupport; import com.lunarclient.apollo.module.tebex.TebexEmbeddedCheckoutSupport; import com.lunarclient.apollo.option.Option; @@ -133,9 +134,10 @@ default boolean hasPermission(Options options, Option option) { * Returns a {@link List} of {@link LunarClientMod} the player has installed. * * @return the installed mods + * @deprecated for removal since 1.2.5, use {@link ModSettingModule#requestInstalledMods(ApolloPlayer)} instead. * @since 1.1.6 */ - @Nullable List getInstalledMods(); + @Deprecated @Nullable List getInstalledMods(); /** * Returns the {@link TebexEmbeddedCheckoutSupport} type. diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java index bc70306b..f0d5bce8 100644 --- a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java +++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java @@ -24,6 +24,8 @@ package com.lunarclient.apollo.roundtrip; import com.lunarclient.apollo.async.future.UncertainFuture; +import com.lunarclient.apollo.roundtrip.pagination.ApolloPaginatedResponse; +import com.lunarclient.apollo.roundtrip.pagination.ApolloPaginationManager; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -52,6 +54,13 @@ public final class ApolloRoundtripManager { */ private final ScheduledThreadPoolExecutor timeoutExecutor; + /** + * The manager for paginated responses. + * + * @since 1.2.5 + */ + private final ApolloPaginationManager paginationManager; + /** * Constructs the {@link ApolloRoundtripManager}. * @@ -60,15 +69,31 @@ public final class ApolloRoundtripManager { public ApolloRoundtripManager() { this.listeners = new ConcurrentHashMap<>(); this.timeoutExecutor = new ScheduledThreadPoolExecutor(1); + this.paginationManager = new ApolloPaginationManager(this); } /** * Handles the given {@link ApolloResponse}. * * @param response the response - * @since 1.0.0 + * @since 1.2.5 */ public void handleResponse(ApolloResponse response) { + if (response instanceof ApolloPaginatedResponse) { + this.paginationManager.handlePage((ApolloPaginatedResponse) response); + return; + } + + this.completeFuture(response); + } + + /** + * Completes the {@link UncertainFuture} for a given {@link ApolloResponse}. + * + * @param response the response + * @since 1.2.5 + */ + public void completeFuture(ApolloResponse response) { UncertainFuture future = this.listeners.remove(response.getPacketId()); if (future != null) { @@ -91,6 +116,7 @@ public void registerListener(ApolloRequest request this.timeoutExecutor.schedule(() -> { try { UncertainFuture listener = this.listeners.remove(packetId); + this.paginationManager.handleTimeout(packetId); if (listener != null) { Throwable error = new Throwable("Timeout exceeded!"); diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginatedResponse.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginatedResponse.java new file mode 100644 index 00000000..852a055a --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginatedResponse.java @@ -0,0 +1,86 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.roundtrip.pagination; + +import com.lunarclient.apollo.roundtrip.ApolloResponse; +import java.util.List; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +/** + * Represents a Paginated Apollo Response. + * + * @param the type of the elements in the paginated payload + * @since 1.2.5 + */ +@Getter +@SuperBuilder +public abstract class ApolloPaginatedResponse extends ApolloResponse { + + /** + * The current page number. + * + * @since 1.2.5 + */ + @Getter(AccessLevel.PROTECTED) + int page; + + /** + * The total number of pages expected. + * + * @since 1.2.5 + */ + @Getter(AccessLevel.PROTECTED) + int totalPages; + + /** + * The elements contained in this specific page. + * + * @since 1.2.5 + */ + List elements; + + /** + * Checks if this is the final page in the sequence. + * + * @return true if it is the last page + * @since 1.2.5 + */ + public boolean isLastPage() { + return this.page == this.totalPages - 1; + } + + /** + * Creates a finalized response containing all paginated elements. + * + * @param packetId the response packet id + * @param elements the complete list of elements across all pages + * @return a new combined response + * @since 1.2.5 + */ + public abstract ApolloPaginatedResponse combine(UUID packetId, List elements); + +} diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginationManager.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginationManager.java new file mode 100644 index 00000000..46f1dc4b --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/pagination/ApolloPaginationManager.java @@ -0,0 +1,91 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.roundtrip.pagination; + +import com.lunarclient.apollo.roundtrip.ApolloRoundtripManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Handles the accumulation of paginated Apollo responses. + * + * @since 1.2.5 + */ +public final class ApolloPaginationManager { + + private final Map> pagination; + private final ApolloRoundtripManager roundtripManager; + + /** + * Constructs the {@link ApolloPaginationManager}. + * + * @param roundtripManager the round trip manager + * @since 1.2.5 + */ + public ApolloPaginationManager(ApolloRoundtripManager roundtripManager) { + this.pagination = new ConcurrentHashMap<>(); + this.roundtripManager = roundtripManager; + } + + /** + * Processes a single page. If it's the last page, it combines the data + * and passes it back to the main round-trip manager to complete the future. + * + * @param response the paginated response + * @param the paginated response type + * @since 1.2.5 + */ + @SuppressWarnings("unchecked") + public void handlePage(ApolloPaginatedResponse response) { + UUID packetId = response.getPacketId(); + + List elements = (List) this.pagination.computeIfAbsent(packetId, k -> new ArrayList<>()); + if (response.getElements() != null) { + elements.addAll(response.getElements()); + } + + if (!response.isLastPage()) { + return; + } + + this.pagination.remove(packetId); + + ApolloPaginatedResponse combinedResponse = response.combine(packetId, elements); + this.roundtripManager.completeFuture(combinedResponse); + } + + /** + * Clears pending data for a request that timed out. + * + * @param packetId the request packet id + * @since 1.2.5 + */ + public void handleTimeout(UUID packetId) { + this.pagination.remove(packetId); + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/modsettings/ModSettingModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/modsettings/ModSettingModuleImpl.java index 477c6348..50e1c963 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/modsettings/ModSettingModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/modsettings/ModSettingModuleImpl.java @@ -24,22 +24,32 @@ package com.lunarclient.apollo.module.modsettings; import com.google.protobuf.Any; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Value; import com.lunarclient.apollo.ApolloManager; +import com.lunarclient.apollo.async.Future; +import com.lunarclient.apollo.client.mod.LunarClientMod; +import com.lunarclient.apollo.client.mod.LunarClientModType; import com.lunarclient.apollo.configurable.v1.ConfigurableSettings; import com.lunarclient.apollo.configurable.v1.OverrideConfigurableSettingsMessage; import com.lunarclient.apollo.event.ApolloReceivePacketEvent; import com.lunarclient.apollo.event.EventBus; import com.lunarclient.apollo.event.modsetting.ApolloUpdateModOptionEvent; +import com.lunarclient.apollo.modsetting.v1.Mod; +import com.lunarclient.apollo.module.modsetting.InstalledModsRequest; +import com.lunarclient.apollo.module.modsetting.InstalledModsResponse; import com.lunarclient.apollo.module.modsetting.ModSettingModule; import com.lunarclient.apollo.network.NetworkOptions; import com.lunarclient.apollo.option.Option; import com.lunarclient.apollo.option.StatusOptionsImpl; +import com.lunarclient.apollo.player.AbstractApolloPlayer; import com.lunarclient.apollo.player.ApolloPlayer; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; import lombok.NonNull; import org.jetbrains.annotations.NotNull; @@ -50,6 +60,8 @@ */ public final class ModSettingModuleImpl extends ModSettingModule { + private static final LunarClientModType[] MOD_TYPES = LunarClientModType.values(); + /** * Creates a new instance of {@link ModSettingModuleImpl}. * @@ -66,13 +78,49 @@ public ModSettingModuleImpl() { return ApolloManager.getModsManager().getPlayerOptions().get(player, option); } + @Override + public Future requestInstalledMods(@NotNull ApolloPlayer player) { + InstalledModsRequest request = InstalledModsRequest.builder().build(); + + com.lunarclient.apollo.modsetting.v1.InstalledModsRequest requestProto = com.lunarclient.apollo.modsetting.v1.InstalledModsRequest.newBuilder() + .setRequestId(ByteString.copyFromUtf8(request.getRequestId().toString())) + .build(); + + return ((AbstractApolloPlayer) player).sendRoundTripPacket(request, requestProto); + } + private void onReceivePacket(ApolloReceivePacketEvent event) { ApolloPlayer player = event.getPlayer(); - Any packet = event.getPacket(); + Any any = event.getPacket(); - if(packet.is(OverrideConfigurableSettingsMessage.class) || packet.is(ConfigurableSettings.class)) { - this.handleConfiguration(player, packet); + if (any.is(OverrideConfigurableSettingsMessage.class) || any.is(ConfigurableSettings.class)) { + this.handleConfiguration(player, any); } + + event.unpack(com.lunarclient.apollo.modsetting.v1.InstalledModsResponse.class).ifPresent(packet -> { + List mods = packet.getModGroupsList().stream() + .flatMap(group -> group.getModsList().stream() + .map(mod -> this.fromProtobuf(MOD_TYPES[group.getTypeValue() - 1], mod))) + .collect(Collectors.toList()); + + InstalledModsResponse response = InstalledModsResponse.builder() + .packetId(UUID.fromString(packet.getRequestId().toStringUtf8())) + .page(packet.getPage()) + .totalPages(packet.getTotalPages()) + .elements(mods) + .build(); + + ApolloManager.getRoundtripManager().handleResponse(response); + }); + } + + private LunarClientMod fromProtobuf(LunarClientModType type, Mod mod) { + return LunarClientMod.builder() + .id(mod.getId()) + .displayName(mod.getId()) + .version(mod.getVersion()) + .type(type) + .build(); } private void handleConfiguration(ApolloPlayer player, Any any) { diff --git a/common/src/main/java/com/lunarclient/apollo/player/AbstractApolloPlayer.java b/common/src/main/java/com/lunarclient/apollo/player/AbstractApolloPlayer.java index 3c383e45..b4ddf62d 100644 --- a/common/src/main/java/com/lunarclient/apollo/player/AbstractApolloPlayer.java +++ b/common/src/main/java/com/lunarclient/apollo/player/AbstractApolloPlayer.java @@ -52,7 +52,7 @@ public abstract class AbstractApolloPlayer implements ApolloPlayer { private MinecraftVersion minecraftVersion; private LunarClientVersion lunarClientVersion; - private List installedMods; + @Deprecated private List installedMods; private TebexEmbeddedCheckoutSupport tebexEmbeddedCheckoutSupport; private PayNowEmbeddedCheckoutSupport payNowEmbeddedCheckoutSupport; diff --git a/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java b/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java index 71ddfaf0..329c519c 100644 --- a/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java @@ -25,8 +25,6 @@ import com.google.protobuf.Value; import com.lunarclient.apollo.Apollo; -import com.lunarclient.apollo.client.mod.LunarClientMod; -import com.lunarclient.apollo.client.mod.LunarClientModType; import com.lunarclient.apollo.client.version.LunarClientVersion; import com.lunarclient.apollo.client.version.MinecraftVersion; import com.lunarclient.apollo.event.EventBus; @@ -39,14 +37,13 @@ import com.lunarclient.apollo.module.tebex.TebexEmbeddedCheckoutSupport; import com.lunarclient.apollo.network.NetworkOptions; import com.lunarclient.apollo.player.v1.PlayerHandshakeMessage; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; import lombok.NoArgsConstructor; import lombok.NonNull; import org.jetbrains.annotations.NotNull; @@ -131,16 +128,6 @@ public void handlePlayerHandshake(@NotNull ApolloPlayer player, @NotNull PlayerH .semVer(message.getLunarClientVersion().getSemver()) .build(); - LunarClientModType[] modTypes = LunarClientModType.values(); - List mods = message.getInstalledModsList().stream().map(mod -> - LunarClientMod.builder() - .id(mod.getId()) - .displayName(mod.getName()) - .version(mod.getVersion()) - .type(modTypes[mod.getTypeValue() - 1]) - .build() - ).collect(Collectors.toList()); - int embeddedCheckoutSupport = message.getEmbeddedCheckoutSupportValue(); TebexEmbeddedCheckoutSupport tebexEmbeddedCheckoutSupport; try { @@ -159,7 +146,6 @@ public void handlePlayerHandshake(@NotNull ApolloPlayer player, @NotNull PlayerH AbstractApolloPlayer apolloPlayer = ((AbstractApolloPlayer) player); apolloPlayer.setMinecraftVersion(minecraftVersion); apolloPlayer.setLunarClientVersion(lunarClientVersion); - apolloPlayer.setInstalledMods(mods); apolloPlayer.setTebexEmbeddedCheckoutSupport(tebexEmbeddedCheckoutSupport); apolloPlayer.setPayNowEmbeddedCheckoutSupport(payNowEmbeddedCheckoutSupport); @@ -173,7 +159,7 @@ public void handlePlayerHandshake(@NotNull ApolloPlayer player, @NotNull PlayerH } ApolloPlayerHandshakeEvent event = new ApolloPlayerHandshakeEvent( - player, minecraftVersion, lunarClientVersion, mods, + player, minecraftVersion, lunarClientVersion, new ArrayList<>(), tebexEmbeddedCheckoutSupport, payNowEmbeddedCheckoutSupport ); diff --git a/docs/developers/events.mdx b/docs/developers/events.mdx index abeb8307..e3cb1f79 100644 --- a/docs/developers/events.mdx +++ b/docs/developers/events.mdx @@ -46,7 +46,6 @@ _Called when the client sends a PlayerHandshakeMessage._ | `ApolloPlayer player` | The Apollo player that sent the packet. | | `MinecraftVersion minecraftVersion` | The minecraft version the player is running. | | `LunarClientVersion lunarClientVersion` | The Lunar Client version the player is running. | -| `List installedMods` | List of all internal and external mods the player has running.|
diff --git a/docs/developers/lightweight/json/roundtrip-packets.mdx b/docs/developers/lightweight/json/roundtrip-packets.mdx index c5315b4e..cd9c9666 100644 --- a/docs/developers/lightweight/json/roundtrip-packets.mdx +++ b/docs/developers/lightweight/json/roundtrip-packets.mdx @@ -23,6 +23,8 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener { private static ApolloRoundtripJsonListener instance; private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>(); + private final Map>> paginatedFutures = new ConcurrentHashMap<>(); + private final Map> paginatedAccumulator = new ConcurrentHashMap<>(); private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); public ApolloRoundtripJsonListener(ApolloExamplePlugin plugin) { @@ -52,6 +54,9 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener { || "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); } } @@ -75,6 +80,64 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener { return future; } + public CompletableFuture> 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> 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 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 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> future = this.paginatedFutures.remove(requestId); + if (future != null) { + future.complete(accumulated); + } + } + } + private void handleResponse(Player player, UUID requestId, JsonObject message) { Map> futures = this.roundTripPacketFutures.get(player.getUniqueId()); if (futures == null) { diff --git a/docs/developers/lightweight/protobuf/getting-started.mdx b/docs/developers/lightweight/protobuf/getting-started.mdx index e7f159ad..a90ec074 100644 --- a/docs/developers/lightweight/protobuf/getting-started.mdx +++ b/docs/developers/lightweight/protobuf/getting-started.mdx @@ -26,7 +26,7 @@ Available fields for each message, including their types, are available on the B com.lunarclient apollo-protos - 0.0.9 + 0.1.0 ``` @@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api 'com.lunarclient:apollo-protos:0.0.9' + api 'com.lunarclient:apollo-protos:0.1.0' } ``` @@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api("com.lunarclient:apollo-protos:0.0.9") + api("com.lunarclient:apollo-protos:0.1.0") } ``` diff --git a/docs/developers/lightweight/protobuf/roundtrip-packets.mdx b/docs/developers/lightweight/protobuf/roundtrip-packets.mdx index 461a9f30..eacbddde 100644 --- a/docs/developers/lightweight/protobuf/roundtrip-packets.mdx +++ b/docs/developers/lightweight/protobuf/roundtrip-packets.mdx @@ -13,6 +13,8 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener { private static ApolloRoundtripProtoListener instance; private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>(); + private final Map>> paginatedFutures = new ConcurrentHashMap<>(); + private final Map> paginatedAccumulator = new ConcurrentHashMap<>(); private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) { @@ -33,6 +35,10 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener { 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) { @@ -60,6 +66,41 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener { return future; } + public CompletableFuture> sendPaginatedRequest(Player player, UUID requestId, GeneratedMessageV3 request) { + ProtobufPacketUtil.sendPacket(player, request); + + CompletableFuture> 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 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> future = this.paginatedFutures.remove(requestId); + if (future != null) { + future.complete(accumulated); + } + } + } + private void handleResponse(Player player, UUID requestId, T message) { Map> futures = this.roundTripPacketFutures.get(player.getUniqueId()); if (futures == null) { diff --git a/docs/developers/modules/modsetting.mdx b/docs/developers/modules/modsetting.mdx index ca315a62..a4bf847a 100644 --- a/docs/developers/modules/modsetting.mdx +++ b/docs/developers/modules/modsetting.mdx @@ -118,6 +118,32 @@ public void broadcastDisableLightingModExample(Player viewer) { } ``` +**Request Installed Mods** + +```java +public void requestInstalledModsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + if (!apolloPlayerOpt.isPresent()) { + viewer.sendMessage("Join with Lunar Client to test this feature!"); + return; + } + + this.modSettingModule.requestInstalledMods(apolloPlayerOpt.get()) + .onSuccess(response -> { + List modIds = response.getElements() + .stream().map(LunarClientMod::getId) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }) + .onFailure(exception -> { + viewer.sendMessage("Internal error! Check console!"); + exception.printStackTrace(); + }); +} +``` + @@ -159,6 +185,30 @@ public void broadcastDisableLightingModExample() { } ``` +**Request Installed Mods** + +```java +public void requestInstalledModsExample(Player viewer) { + UUID requestId = UUID.randomUUID(); + + InstalledModsRequest request = InstalledModsRequest.newBuilder() + .setRequestId(ByteString.copyFromUtf8(requestId.toString())) + .build(); + + ApolloRoundtripProtoListener.getInstance().sendPaginatedRequest(viewer, requestId, request) + .thenAccept(mods -> { + List modIds = mods.stream() + .map(Mod::getId) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }).exceptionally(throwable -> { + viewer.sendMessage("Failed to receive a response in time."); + return null; + }); +} +``` + @@ -200,6 +250,27 @@ public void broadcastDisableLightingModExample() { } ``` +**Request Installed Mods** + +```java +public void requestInstalledModsExample(Player viewer) { + UUID requestId = UUID.randomUUID(); + + ApolloRoundtripJsonListener.getInstance() + .sendPaginatedRequest(viewer, requestId, new JsonObject(), "lunarclient.apollo.modsetting.v1.InstalledModsRequest") + .thenAccept(mods -> { + List modIds = mods.stream() + .map(mod -> mod.get("id").getAsString()) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }).exceptionally(throwable -> { + viewer.sendMessage("Failed to receive a response in time."); + return null; + }); +} +``` + diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ModSettingsApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ModSettingsApiExample.java index 53247cf7..76caec20 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ModSettingsApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ModSettingsApiExample.java @@ -24,11 +24,14 @@ package com.lunarclient.apollo.example.api.module; import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.client.mod.LunarClientMod; import com.lunarclient.apollo.example.module.impl.ModSettingsExample; import com.lunarclient.apollo.mods.impl.ModLighting; import com.lunarclient.apollo.module.modsetting.ModSettingModule; import com.lunarclient.apollo.player.ApolloPlayer; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.bukkit.entity.Player; public class ModSettingsApiExample extends ModSettingsExample { @@ -53,4 +56,27 @@ public void broadcastDisableLightingModExample() { this.modSettingModule.getOptions().set(ModLighting.ENABLED, false); } + @Override + public void requestInstalledModsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + if (!apolloPlayerOpt.isPresent()) { + viewer.sendMessage("Join with Lunar Client to test this feature!"); + return; + } + + this.modSettingModule.requestInstalledMods(apolloPlayerOpt.get()) + .onSuccess(response -> { + List modIds = response.getElements() + .stream().map(LunarClientMod::getId) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }) + .onFailure(exception -> { + viewer.sendMessage("Internal error! Check console!"); + exception.printStackTrace(); + }); + } + } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ModSettingsCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ModSettingsCommand.java index 96872f46..b62551fa 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ModSettingsCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ModSettingsCommand.java @@ -44,6 +44,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command if (args.length != 1) { player.sendMessage("Usage: /modsettings "); + player.sendMessage("Usage: /modsettings "); return true; } @@ -68,8 +69,15 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command break; } + case "requestinstalledmods": { + modSettingsExample.requestInstalledModsExample(player); + player.sendMessage("Requesting installed mods....."); + break; + } + default: { player.sendMessage("Usage: /modsettings "); + player.sendMessage("Usage: /modsettings "); break; } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ModSettingsExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ModSettingsExample.java index e9a35e10..71777e2d 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ModSettingsExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ModSettingsExample.java @@ -34,4 +34,6 @@ public abstract class ModSettingsExample extends ApolloModuleExample { public abstract void broadcastDisableLightingModExample(); + public abstract void requestInstalledModsExample(Player viewer); + } diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloRoundtripJsonListener.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloRoundtripJsonListener.java index c9ad3813..1c2ff26f 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloRoundtripJsonListener.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloRoundtripJsonListener.java @@ -23,11 +23,15 @@ */ package com.lunarclient.apollo.example.json.listener; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.json.util.JsonPacketUtil; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -51,6 +55,8 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener { private static ApolloRoundtripJsonListener instance; private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>(); + private final Map>> paginatedFutures = new ConcurrentHashMap<>(); + private final Map> paginatedAccumulator = new ConcurrentHashMap<>(); private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); public ApolloRoundtripJsonListener(ApolloExamplePlugin plugin) { @@ -79,6 +85,9 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla || "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); } } @@ -102,6 +111,64 @@ public CompletableFuture sendRequest(Player player, UUID requestId, return future; } + public CompletableFuture> 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> 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 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 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> future = this.paginatedFutures.remove(requestId); + if (future != null) { + future.complete(accumulated); + } + } + } + private void handleResponse(Player player, UUID requestId, JsonObject message) { Map> futures = this.roundTripPacketFutures.get(player.getUniqueId()); if (futures == null) { diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ModSettingsJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ModSettingsJsonExample.java index bb245df1..d260febf 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ModSettingsJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ModSettingsJsonExample.java @@ -24,11 +24,15 @@ package com.lunarclient.apollo.example.json.module; import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.listener.ApolloRoundtripJsonListener; import com.lunarclient.apollo.example.json.util.JsonPacketUtil; import com.lunarclient.apollo.example.json.util.JsonUtil; import com.lunarclient.apollo.example.module.impl.ModSettingsExample; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; import org.bukkit.entity.Player; public class ModSettingsJsonExample extends ModSettingsExample { @@ -61,4 +65,22 @@ public void broadcastDisableLightingModExample() { JsonPacketUtil.broadcastPacket(message); } + @Override + public void requestInstalledModsExample(Player viewer) { + UUID requestId = UUID.randomUUID(); + + ApolloRoundtripJsonListener.getInstance() + .sendPaginatedRequest(viewer, requestId, new JsonObject(), "lunarclient.apollo.modsetting.v1.InstalledModsRequest") + .thenAccept(mods -> { + List modIds = mods.stream() + .map(mod -> mod.get("id").getAsString()) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }).exceptionally(throwable -> { + viewer.sendMessage("Failed to receive a response in time."); + return null; + }); + } + } diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloRoundtripProtoListener.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloRoundtripProtoListener.java index 97a4e1e4..03d43c72 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloRoundtripProtoListener.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloRoundtripProtoListener.java @@ -28,8 +28,12 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.modsetting.v1.InstalledModsResponse; +import com.lunarclient.apollo.modsetting.v1.Mod; import com.lunarclient.apollo.transfer.v1.PingResponse; import com.lunarclient.apollo.transfer.v1.TransferResponse; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -50,6 +54,8 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener { private static ApolloRoundtripProtoListener instance; private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>(); + private final Map>> paginatedFutures = new ConcurrentHashMap<>(); + private final Map> paginatedAccumulator = new ConcurrentHashMap<>(); private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) { @@ -69,6 +75,10 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla 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) { @@ -96,6 +106,41 @@ public CompletableFuture sendRequest(Player pl return future; } + public CompletableFuture> sendPaginatedRequest(Player player, UUID requestId, GeneratedMessageV3 request) { + ProtobufPacketUtil.sendPacket(player, request); + + CompletableFuture> 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 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> future = this.paginatedFutures.remove(requestId); + if (future != null) { + future.complete(accumulated); + } + } + } + private void handleResponse(Player player, UUID requestId, T message) { Map> futures = this.roundTripPacketFutures.get(player.getUniqueId()); if (futures == null) { diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ModSettingsProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ModSettingsProtoExample.java index c3ebf501..04780af3 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ModSettingsProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ModSettingsProtoExample.java @@ -23,13 +23,20 @@ */ package com.lunarclient.apollo.example.proto.module; +import com.google.protobuf.ByteString; import com.google.protobuf.NullValue; import com.google.protobuf.Value; import com.lunarclient.apollo.configurable.v1.ConfigurableSettings; import com.lunarclient.apollo.example.module.impl.ModSettingsExample; +import com.lunarclient.apollo.example.proto.listener.ApolloRoundtripProtoListener; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.modsetting.v1.InstalledModsRequest; +import com.lunarclient.apollo.modsetting.v1.Mod; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; import org.bukkit.entity.Player; public class ModSettingsProtoExample extends ModSettingsExample { @@ -62,4 +69,25 @@ public void broadcastDisableLightingModExample() { ProtobufPacketUtil.broadcastPacket(settings); } + @Override + public void requestInstalledModsExample(Player viewer) { + UUID requestId = UUID.randomUUID(); + + InstalledModsRequest request = InstalledModsRequest.newBuilder() + .setRequestId(ByteString.copyFromUtf8(requestId.toString())) + .build(); + + ApolloRoundtripProtoListener.getInstance().sendPaginatedRequest(viewer, requestId, request) + .thenAccept(mods -> { + List modIds = mods.stream() + .map(Mod::getId) + .collect(Collectors.toList()); + + viewer.sendMessage("Found " + modIds.size() + " mods: " + modIds); + }).exceptionally(throwable -> { + viewer.sendMessage("Failed to receive a response in time."); + return null; + }); + } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13049556..573ee4a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ geantyref = "1.3.11" idea = "1.1.7" jetbrains = "24.0.1" lombok = "1.18.38" -protobuf = "0.0.9" +protobuf = "0.1.0" gson = "2.10.1" shadow = "8.1.1" spotless = "6.13.0"