diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 4a465fe7..f65bf2a7 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -41,7 +41,7 @@ jobs:
fi
- name: Gradle Publish
- if: "${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/version/') || github.ref == 'refs/heads/feature/totem-counter-module' }}"
+ if: "${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/version/') }}"
run: ./gradlew publish
- name: Gradle Release
diff --git a/api/src/main/java/com/lunarclient/apollo/event/modsetting/ApolloUpdateModOptionEvent.java b/api/src/main/java/com/lunarclient/apollo/event/modsetting/ApolloUpdateModOptionEvent.java
index d8724a6d..0199fabb 100644
--- a/api/src/main/java/com/lunarclient/apollo/event/modsetting/ApolloUpdateModOptionEvent.java
+++ b/api/src/main/java/com/lunarclient/apollo/event/modsetting/ApolloUpdateModOptionEvent.java
@@ -24,17 +24,27 @@
package com.lunarclient.apollo.event.modsetting;
import com.lunarclient.apollo.event.Event;
+import com.lunarclient.apollo.module.RestrictedAccess;
+import com.lunarclient.apollo.module.modsetting.ModSettingModule;
import com.lunarclient.apollo.option.Option;
import com.lunarclient.apollo.player.ApolloPlayer;
import lombok.Value;
import org.jetbrains.annotations.Nullable;
/**
- * Represents an event that is fired when an option is updated.
+ * Represents an event that is fired when a player's Lunar Client mod option is updated.
+ *
+ *
This event is fired for live updates only, not for the options listed in a
+ * {@link ModSettingModule#requestModStatus(ApolloPlayer)} response.
+ *
+ * This event is part of the private Mod Status API of {@link ModSettingModule}. Lunar Client
+ * only reports mod status to servers that have been granted access, so this event is never
+ * fired on other servers.
*
* @since 1.2.1
*/
@Value
+@RestrictedAccess
public class ApolloUpdateModOptionEvent implements Event {
/**
diff --git a/api/src/main/java/com/lunarclient/apollo/module/RestrictedAccess.java b/api/src/main/java/com/lunarclient/apollo/module/RestrictedAccess.java
new file mode 100644
index 00000000..46c68062
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/RestrictedAccess.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks access restricted Apollo API, either a whole module or part of a public module.
+ *
+ * Restricted API only works on servers approved by the Lunar Client team. Lunar Client
+ * enforces this, so without approval the API does not throw, it just has no effect.
+ *
+ * See Private Modules
+ * to request access.
+ *
+ * @since 1.3.0
+ */
+@Documented
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RestrictedAccess {
+ // Marker annotation.
+}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java
index 45783a13..dd20a602 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/cosmetic/CosmeticModule.java
@@ -26,6 +26,7 @@
import com.lunarclient.apollo.common.location.ApolloBlockLocation;
import com.lunarclient.apollo.module.ApolloModule;
import com.lunarclient.apollo.module.ModuleDefinition;
+import com.lunarclient.apollo.module.RestrictedAccess;
import com.lunarclient.apollo.module.cosmetic.options.BodyOptions;
import com.lunarclient.apollo.module.cosmetic.options.CloakOptions;
import com.lunarclient.apollo.module.cosmetic.options.CosmeticOptions;
@@ -41,8 +42,11 @@
/**
* Represents the cosmetic module.
*
+ * Access to this module is {@link RestrictedAccess restricted}.
+ *
* @since 1.2.6
*/
+@RestrictedAccess
@ApiStatus.NonExtendable
@ModuleDefinition(id = "cosmetic", name = "Cosmetic")
public abstract class CosmeticModule extends ApolloModule {
diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModOptionStatus.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModOptionStatus.java
new file mode 100644
index 00000000..a294baf5
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModOptionStatus.java
@@ -0,0 +1,57 @@
+/*
+ * 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.module.RestrictedAccess;
+import com.lunarclient.apollo.option.Option;
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * Represents the current value of a Lunar Client mod option.
+ *
+ * @since 1.3.0
+ */
+@Getter
+@Builder
+@RestrictedAccess
+public final class ModOptionStatus {
+
+ /**
+ * Returns the mod {@link Option}.
+ *
+ * @return the option
+ * @since 1.3.0
+ */
+ Option, ?, ?> option;
+
+ /**
+ * Returns the current {@link Object} value of the option.
+ *
+ * @return the value
+ * @since 1.3.0
+ */
+ Object value;
+
+}
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 c166aed9..22fff0ce 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
@@ -27,6 +27,7 @@
import com.lunarclient.apollo.async.Future;
import com.lunarclient.apollo.module.ApolloModule;
import com.lunarclient.apollo.module.ModuleDefinition;
+import com.lunarclient.apollo.module.RestrictedAccess;
import com.lunarclient.apollo.option.Option;
import com.lunarclient.apollo.player.ApolloPlayer;
import com.lunarclient.apollo.util.ConfigTarget;
@@ -65,6 +66,7 @@ public boolean isClientNotify() {
* @return the value of the option
* @since 1.2.1
*/
+ @RestrictedAccess
public abstract > T getStatus(@NotNull ApolloPlayer player, @NonNull C option);
/**
@@ -76,4 +78,16 @@ public boolean isClientNotify() {
*/
public abstract Future requestInstalledMods(ApolloPlayer player);
+ /**
+ * Sends the {@link ModStatusRequest} to the {@link ApolloPlayer}.
+ *
+ * The response also updates {@link #getStatus(ApolloPlayer, Option)}.
+ *
+ * @param player the player
+ * @return future to be listened to for errors/success
+ * @since 1.3.0
+ */
+ @RestrictedAccess
+ public abstract Future requestModStatus(ApolloPlayer player);
+
}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusRequest.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusRequest.java
new file mode 100644
index 00000000..1c5b635b
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusRequest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.module.RestrictedAccess;
+import com.lunarclient.apollo.roundtrip.ApolloRequest;
+import lombok.Builder;
+
+/**
+ * Represents the mod status request.
+ *
+ * @since 1.3.0
+ */
+@Builder
+@RestrictedAccess
+public final class ModStatusRequest extends ApolloRequest {
+
+}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusResponse.java b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusResponse.java
new file mode 100644
index 00000000..f31373ab
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/modsetting/ModStatusResponse.java
@@ -0,0 +1,54 @@
+/*
+ * 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.module.RestrictedAccess;
+import com.lunarclient.apollo.roundtrip.pagination.ApolloPaginatedResponse;
+import java.util.List;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * Represents the mod status response.
+ *
+ * The elements are the mod options the player has changed from their default
+ * value. Any option that is not listed is at its default value.
+ *
+ * @since 1.3.0
+ */
+@Getter
+@SuperBuilder
+@RestrictedAccess
+public final class ModStatusResponse extends ApolloPaginatedResponse {
+
+ @Override
+ public ApolloPaginatedResponse combine(UUID packetId, List elements) {
+ return ModStatusResponse.builder()
+ .packetId(packetId)
+ .elements(elements)
+ .build();
+ }
+
+}
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 50e1c963..278b33ca 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
@@ -39,12 +39,16 @@
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.ModOptionStatus;
import com.lunarclient.apollo.module.modsetting.ModSettingModule;
+import com.lunarclient.apollo.module.modsetting.ModStatusRequest;
+import com.lunarclient.apollo.module.modsetting.ModStatusResponse;
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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -89,6 +93,17 @@ public Future requestInstalledMods(@NotNull ApolloPlayer
return ((AbstractApolloPlayer) player).sendRoundTripPacket(request, requestProto);
}
+ @Override
+ public Future requestModStatus(@NotNull ApolloPlayer player) {
+ ModStatusRequest request = ModStatusRequest.builder().build();
+
+ com.lunarclient.apollo.modsetting.v1.ModStatusRequest requestProto = com.lunarclient.apollo.modsetting.v1.ModStatusRequest.newBuilder()
+ .setRequestId(ByteString.copyFromUtf8(request.getRequestId().toString()))
+ .build();
+
+ return ((AbstractApolloPlayer) player).sendRoundTripPacket(request, requestProto);
+ }
+
private void onReceivePacket(ApolloReceivePacketEvent event) {
ApolloPlayer player = event.getPlayer();
Any any = event.getPacket();
@@ -112,6 +127,17 @@ private void onReceivePacket(ApolloReceivePacketEvent event) {
ApolloManager.getRoundtripManager().handleResponse(response);
});
+
+ event.unpack(com.lunarclient.apollo.modsetting.v1.ModStatusResponse.class).ifPresent(packet -> {
+ ModStatusResponse response = ModStatusResponse.builder()
+ .packetId(UUID.fromString(packet.getRequestId().toStringUtf8()))
+ .page(packet.getPage())
+ .totalPages(packet.getTotalPages())
+ .elements(this.updateModStatus(player, packet))
+ .build();
+
+ ApolloManager.getRoundtripManager().handleResponse(response);
+ });
}
private LunarClientMod fromProtobuf(LunarClientModType type, Mod mod) {
@@ -188,4 +214,32 @@ public void updateOptions(ApolloPlayer player, Map properties, bo
}
}
+ private List updateModStatus(ApolloPlayer player, com.lunarclient.apollo.modsetting.v1.ModStatusResponse packet) {
+ StatusOptionsImpl statusOptions = ApolloManager.getModsManager().getPlayerOptions();
+
+ if (packet.getPage() == 0) {
+ statusOptions.getPlayerOptions().remove(player.getUniqueId());
+ }
+
+ List elements = new ArrayList<>();
+ for (com.lunarclient.apollo.modsetting.v1.ModOptionStatus status : packet.getModOptionsList()) {
+ Option, ?, ?> option = statusOptions.getRegistry().get(status.getKey());
+
+ // Option exists on the client but doesn't exist in the API yet.
+ if (option == null) {
+ continue;
+ }
+
+ Object value = NetworkOptions.unwrapValue(status.getValue(), option.getTypeToken().getType());
+ statusOptions.set(player, option, value);
+
+ elements.add(ModOptionStatus.builder()
+ .option(option)
+ .value(value)
+ .build());
+ }
+
+ return elements;
+ }
+
}
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 329c519c..8e2af840 100644
--- a/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java
+++ b/common/src/main/java/com/lunarclient/apollo/player/ApolloPlayerManagerImpl.java
@@ -23,7 +23,6 @@
*/
package com.lunarclient.apollo.player;
-import com.google.protobuf.Value;
import com.lunarclient.apollo.Apollo;
import com.lunarclient.apollo.client.version.LunarClientVersion;
import com.lunarclient.apollo.client.version.MinecraftVersion;
@@ -31,8 +30,6 @@
import com.lunarclient.apollo.event.player.ApolloPlayerHandshakeEvent;
import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent;
import com.lunarclient.apollo.event.player.ApolloUnregisterPlayerEvent;
-import com.lunarclient.apollo.module.modsetting.ModSettingModule;
-import com.lunarclient.apollo.module.modsettings.ModSettingModuleImpl;
import com.lunarclient.apollo.module.paynow.PayNowEmbeddedCheckoutSupport;
import com.lunarclient.apollo.module.tebex.TebexEmbeddedCheckoutSupport;
import com.lunarclient.apollo.network.NetworkOptions;
@@ -149,15 +146,6 @@ public void handlePlayerHandshake(@NotNull ApolloPlayer player, @NotNull PlayerH
apolloPlayer.setTebexEmbeddedCheckoutSupport(tebexEmbeddedCheckoutSupport);
apolloPlayer.setPayNowEmbeddedCheckoutSupport(payNowEmbeddedCheckoutSupport);
- Map modStatus = message.getModStatusMap();
- if (!modStatus.isEmpty()) {
- ModSettingModuleImpl modSettingModule = (ModSettingModuleImpl) Apollo.getModuleManager().getModule(ModSettingModule.class);
-
- if (modSettingModule.isEnabled()) {
- modSettingModule.updateOptions(apolloPlayer, modStatus, false);
- }
- }
-
ApolloPlayerHandshakeEvent event = new ApolloPlayerHandshakeEvent(
player, minecraftVersion, lunarClientVersion, new ArrayList<>(),
tebexEmbeddedCheckoutSupport, payNowEmbeddedCheckoutSupport
diff --git a/docs/acceptable-use.mdx b/docs/acceptable-use.mdx
index 49226e1c..e640360a 100644
--- a/docs/acceptable-use.mdx
+++ b/docs/acceptable-use.mdx
@@ -13,7 +13,7 @@ Apollo includes a wide range of publicly available modules that can be used by a
The intent of Apollo is to encourage servers to integrate directly with Lunar Client to create better player experiences.
## Private & Future Modules
-Apollo includes [private modules](/apollo/developers/private-modules) that require additional approval before use.
+Apollo includes [private modules and features](/apollo/developers/private-modules) that require additional approval before use.
We typically reserve private modules for features that:
* Something that requires backing infrastructure that we're unable to provide for free.
diff --git a/docs/developers/events.mdx b/docs/developers/events.mdx
index 358b356a..5287787e 100644
--- a/docs/developers/events.mdx
+++ b/docs/developers/events.mdx
@@ -186,6 +186,8 @@ _Called when a Lunar Client Mod option is updated._
| `Option, ?, ?> option` | The option that was updated. |
| `Object value` | The new value of the option. |
+This event is part of the private [Mod Status API](/apollo/developers/private-modules/modstatus) and is only called on servers that have been granted access.
+
diff --git a/docs/developers/lightweight/json/roundtrip-packets.mdx b/docs/developers/lightweight/json/roundtrip-packets.mdx
index cd9c9666..a4966284 100644
--- a/docs/developers/lightweight/json/roundtrip-packets.mdx
+++ b/docs/developers/lightweight/json/roundtrip-packets.mdx
@@ -57,6 +57,9 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener {
} else if ("lunarclient.apollo.modsetting.v1.InstalledModsResponse".equals(type)) {
UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
this.parseModGroups(requestId, payload);
+ } else if ("lunarclient.apollo.modsetting.v1.ModStatusResponse".equals(type)) {
+ UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
+ this.parseModStatus(requestId, payload);
}
}
@@ -124,6 +127,19 @@ public class ApolloRoundtripJsonListener implements PluginMessageListener {
this.handlePage(requestId, accumulated, response);
}
+ private void parseModStatus(UUID requestId, JsonObject response) {
+ List accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
+
+ JsonArray modOptions = response.getAsJsonArray("mod_options");
+ if (modOptions != null) {
+ for (JsonElement option : modOptions) {
+ accumulated.add(option.getAsJsonObject());
+ }
+ }
+
+ 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;
diff --git a/docs/developers/lightweight/protobuf/roundtrip-packets.mdx b/docs/developers/lightweight/protobuf/roundtrip-packets.mdx
index eacbddde..f486492d 100644
--- a/docs/developers/lightweight/protobuf/roundtrip-packets.mdx
+++ b/docs/developers/lightweight/protobuf/roundtrip-packets.mdx
@@ -15,6 +15,8 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener {
private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>();
private final Map>> paginatedFutures = new ConcurrentHashMap<>();
private final Map> paginatedAccumulator = new ConcurrentHashMap<>();
+ private final Map>> modStatusFutures = new ConcurrentHashMap<>();
+ private final Map> modStatusAccumulator = new ConcurrentHashMap<>();
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) {
@@ -39,6 +41,10 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener {
InstalledModsResponse message = any.unpack(InstalledModsResponse.class);
UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8());
this.handlePagedResponse(requestId, message);
+ } else if (any.is(ModStatusResponse.class)) {
+ ModStatusResponse message = any.unpack(ModStatusResponse.class);
+ UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8());
+ this.handleModStatusPage(requestId, message);
}
} catch (InvalidProtocolBufferException e) {
@@ -85,6 +91,38 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener {
return future;
}
+ public CompletableFuture> sendModStatusRequest(Player player, UUID requestId, ModStatusRequest request) {
+ ProtobufPacketUtil.sendPacket(player, request);
+
+ CompletableFuture> future = new CompletableFuture<>();
+ this.modStatusFutures.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.modStatusAccumulator.remove(requestId);
+ });
+
+ return future;
+ }
+
+ private void handleModStatusPage(UUID requestId, ModStatusResponse response) {
+ List accumulated = this.modStatusAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
+ accumulated.addAll(response.getModOptionsList());
+
+ if (response.getPage() == response.getTotalPages() - 1) {
+ this.modStatusAccumulator.remove(requestId);
+ CompletableFuture> future = this.modStatusFutures.remove(requestId);
+ if (future != null) {
+ future.complete(accumulated);
+ }
+ }
+ }
+
private void handlePagedResponse(UUID requestId, InstalledModsResponse response) {
List accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
diff --git a/docs/developers/modules.mdx b/docs/developers/modules.mdx
index cdf6d7f8..a6d17d16 100644
--- a/docs/developers/modules.mdx
+++ b/docs/developers/modules.mdx
@@ -4,6 +4,8 @@ Apollo provides a wide-range of public modules that allow you to integrate direc
These modules are available to all servers using Apollo and do not require any pre-approval. You are still expected to follow our [Acceptable Use](/apollo/acceptable-use) guidelines.
+Some public modules also include [private features](/apollo/developers/private-modules) that require approval. These are noted on each module's page.
+
### Available Modules
🔗 [Auto Text Hotkey](/apollo/developers/modules/autotexthotkey)
diff --git a/docs/developers/modules/inventory.mdx b/docs/developers/modules/inventory.mdx
index fcd486eb..9d1ef296 100644
--- a/docs/developers/modules/inventory.mdx
+++ b/docs/developers/modules/inventory.mdx
@@ -1114,7 +1114,7 @@ The same layout built with raw payloads: [apollo-protos implementation](https://
### Minigame Layout
-A minigame overlay: map info, a live kill counter and mod-aware Show Map / Waypoints buttons using client actions, profile, settings and back to lobby buttons.
+A minigame overlay: map info, a live kill counter and mod-aware Show Map / Waypoints buttons using client actions, profile, settings and back to lobby buttons. The mod aware buttons use the private [Mod Status API](/apollo/developers/private-modules/modstatus).

@@ -1227,77 +1227,84 @@ public static void display(InventoryModule inventoryModule, Player viewer) {
.onClick(ApolloButtonAction.runCommand("/settings"))
.build();
- ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
- boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED);
- boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+ List buttons = new ArrayList<>(Arrays.asList(mapInfo, kills, lobby, profile, settings));
+ buttons.addAll(modButtons(apolloPlayer));
+ inventoryModule.displayInventoryButtons(apolloPlayer, buttons);
+ });
+}
- InventoryButton.InventoryButtonBuilder, ?> showMapBuilder = InventoryButton.builder()
- .id("show-map")
- .inventoryType(InventoryType.PLAYER)
- .box(InventoryButtonBox.RIGHT)
- .position(HudPosition.of(6, 52))
- .size(InventoryButton.SIZE_WIDE)
- .shape(ApolloButtonShape.ROUNDED_SQUARE)
- .content(ApolloButtonContent.builder()
- .append(ItemStackIcon.builder()
- .itemName("FILLED_MAP")
- .build())
- .append(Component.text("Show Map"))
- .scale(1.0F)
- .build());
-
- if (minimapEnabled) {
- showMapBuilder
- .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
- .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
- .tooltip(ApolloButtonTooltip.of(
- Component.text("Show Map", NamedTextColor.AQUA),
- Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY)))
- .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW));
- } else {
- showMapBuilder
- .backgroundColor(new Color(224, 64, 64, 128))
- .borderColor(new Color(255, 200, 200, 140))
- .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED)));
- }
+// Displaying these again with the same ids replaces the previous Show Map / Waypoints buttons
+public static List modButtons(ApolloPlayer apolloPlayer) {
+ // getStatus is part of the private Mod Status API: https://lunarclient.dev/apollo/developers/private-modules/modstatus
+ ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
+ boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED);
+ boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+
+ InventoryButton.InventoryButtonBuilder, ?> showMapBuilder = InventoryButton.builder()
+ .id("show-map")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 52))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("FILLED_MAP")
+ .build())
+ .append(Component.text("Show Map"))
+ .scale(1.0F)
+ .build());
- InventoryButton showMap = showMapBuilder.build();
+ if (minimapEnabled) {
+ showMapBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Show Map", NamedTextColor.AQUA),
+ Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW));
+ } else {
+ showMapBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED)));
+ }
- InventoryButton.InventoryButtonBuilder, ?> waypointsBuilder = InventoryButton.builder()
- .id("waypoints")
- .inventoryType(InventoryType.PLAYER)
- .box(InventoryButtonBox.RIGHT)
- .position(HudPosition.of(6, 84))
- .size(InventoryButton.SIZE_WIDE)
- .shape(ApolloButtonShape.ROUNDED_SQUARE)
- .content(ApolloButtonContent.builder()
- .append(ItemStackIcon.builder()
- .itemName("LODESTONE")
- .build())
- .append(Component.text("Waypoints"))
- .scale(1.0F)
- .build());
-
- if (waypointsEnabled) {
- waypointsBuilder
- .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
- .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
- .tooltip(ApolloButtonTooltip.of(
- Component.text("Waypoints", NamedTextColor.GOLD),
- Component.text("Manage your waypoints", NamedTextColor.GRAY)))
- .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU));
- } else {
- waypointsBuilder
- .backgroundColor(new Color(224, 64, 64, 128))
- .borderColor(new Color(255, 200, 200, 140))
- .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED)));
- }
+ InventoryButton showMap = showMapBuilder.build();
+
+ InventoryButton.InventoryButtonBuilder, ?> waypointsBuilder = InventoryButton.builder()
+ .id("waypoints")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 84))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("LODESTONE")
+ .build())
+ .append(Component.text("Waypoints"))
+ .scale(1.0F)
+ .build());
- InventoryButton waypoints = waypointsBuilder.build();
+ if (waypointsEnabled) {
+ waypointsBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Waypoints", NamedTextColor.GOLD),
+ Component.text("Manage your waypoints", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU));
+ } else {
+ waypointsBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED)));
+ }
- inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(mapInfo, kills, lobby,
- profile, settings, showMap, waypoints));
- });
+ InventoryButton waypoints = waypointsBuilder.build();
+
+ return Arrays.asList(showMap, waypoints);
}
private static int getKills(ApolloPlayer apolloViewer) {
diff --git a/docs/developers/modules/modsetting.mdx b/docs/developers/modules/modsetting.mdx
index 127b46e3..215ff90e 100644
--- a/docs/developers/modules/modsetting.mdx
+++ b/docs/developers/modules/modsetting.mdx
@@ -12,7 +12,7 @@ You can find all available mods and their options under the mods section.
- Adds the ability to interact with Lunar Client mods.
- Ability to enable/disable mods.
- Ability to change, enable or disable mod settings within a mod.
- - Ability to retrieve the current value of any mod and settings within a mod.
+ - Ability to retrieve the current value of any mod and settings within a mod ([private feature](/apollo/developers/private-modules/modstatus)).

@@ -22,65 +22,13 @@ You can find all available mods and their options under the mods section.
## Integration
-### Setting Status API
-The **Setting Status API** provides a simple way to retrieve the **current** value of any mod or any setting within a mod.
+### Mod Status API
+The **Mod Status API** allows you to **request** the current value of any mod option, and to listen for updates whenever one changes.
-**Retrieve the current value for options**
-
-```java
-private final ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
-
-private void printOptionStatusExample(Player player) {
- Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(player.getUniqueId());
- apolloPlayerOpt.ifPresent(apolloPlayer -> {
- // https://lunarclient.dev/apollo/developers/mods/waypoints#available-options
- boolean waypointsEnabled = this.modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
-
- // https://lunarclient.dev/apollo/developers/mods/minimap#available-options
- float minimapScale = this.modSettingModule.getStatus(apolloPlayer, ModMinimap.SCALE);
-
- // https://lunarclient.dev/apollo/developers/mods/fov#available-options
- int fovDefaultFov = this.modSettingModule.getStatus(apolloPlayer, ModFov.DEFAULT_FOV);
-
- apolloPlayer.sendMessage(Component.text("Waypoints Enabled: ")
- .append(Component.text(waypointsEnabled)));
-
- apolloPlayer.sendMessage(Component.text("Minimap Scale: ")
- .append(Component.text(minimapScale)));
-
- apolloPlayer.sendMessage(Component.text("Fov Default Fov: ")
- .append(Component.text(fovDefaultFov)));
- });
-}
-```
-
-**Listening for Updates**
-
-Apollo fires the [ApolloUpdateModOptionEvent](/apollo/developers/events), whenever a Lunar Client Mod Option is updated.
-
-```java
-import com.lunarclient.apollo.event.ApolloListener;
-import com.lunarclient.apollo.event.EventBus;
-import com.lunarclient.apollo.event.Listen;
-import com.lunarclient.apollo.event.modsetting.ApolloUpdateModOptionEvent;
-import java.util.Objects;
-import net.kyori.adventure.text.Component;
-
-public class ApolloModStatusExample implements ApolloListener {
-
- public ApolloModStatusExample() {
- EventBus.getBus().register(this);
- }
-
- @Listen
- private void onApolloUpdateModOption(ApolloUpdateModOptionEvent event) {
- event.getPlayer().sendMessage(Component.text(event.getOption().getKey())
- .append(Component.text(" was updated to "))
- .append(Component.text(Objects.toString(event.getValue()))));
- }
-
-}
-```
+
+ The Mod Status API is a **private feature** of this module and not publicly available. The rest of the module, such as the Setting Interception API, stays public.
+ See [Mod Status](/apollo/developers/private-modules/modstatus) for usage, and [Private Modules](/apollo/developers/private-modules#how-to-request-access) to request access.
+
### Setting Interception API
The **Setting Interception API** allows you to **override** the value of any mod option.
diff --git a/docs/developers/private-modules.mdx b/docs/developers/private-modules.mdx
index bf57d20f..8fb7ca6a 100644
--- a/docs/developers/private-modules.mdx
+++ b/docs/developers/private-modules.mdx
@@ -1,18 +1,28 @@
# Private Modules
-Apollo has private modules, that are access restricted extensions of the API.
+Apollo has private modules and private features, which are access restricted parts of the API.
-These modules are only available on pre-approved servers and will not work on servers that have not received approval. These private modules are still subject to our [Acceptable Use](/apollo/acceptable-use) guidelines.
+- A **private module** is restricted as a whole, such as the Cosmetic module.
+- A **private feature** is a restricted part of an otherwise public module, such as the Mod Status API of the Mod Setting module. The rest of that module stays public.
+
+In the Apollo API, private modules and features are marked with the `@RestrictedAccess` annotation. Access is granted separately for each private module and feature.
+
+These modules and features are only available on pre-approved servers and will not work on servers that have not received approval. They are still subject to our [Acceptable Use](/apollo/acceptable-use) guidelines.
### How to Request Access
-We are not currently granting broad access to private modules. However, if you have a specific use-case in mind, you may request access by contacting **apollo@lunarclient.com**.
+We are not currently granting broad access to private modules and features. However, if you have a specific use-case in mind, you may request access by contacting **apollo@lunarclient.com**.
Please include:
- Your server name & domain
+- The private modules and features you need
- A description of your use-case
- Any relevant code snippets (if applicable)
### Available Modules
🔗 [Cosmetic](/apollo/developers/private-modules/cosmetic)
+
+### Available Features
+
+🔗 [Mod Status](/apollo/developers/private-modules/modstatus) (part of the [Mod Setting](/apollo/developers/modules/modsetting) module)
diff --git a/docs/developers/private-modules/_meta.json b/docs/developers/private-modules/_meta.json
index 482e70cb..4c661c01 100644
--- a/docs/developers/private-modules/_meta.json
+++ b/docs/developers/private-modules/_meta.json
@@ -1,3 +1,4 @@
{
- "cosmetic": "Cosmetic"
+ "cosmetic": "Cosmetic",
+ "modstatus": "Mod Status"
}
diff --git a/docs/developers/private-modules/modstatus.mdx b/docs/developers/private-modules/modstatus.mdx
new file mode 100644
index 00000000..48f9fe89
--- /dev/null
+++ b/docs/developers/private-modules/modstatus.mdx
@@ -0,0 +1,181 @@
+import { Callout, Tab, Tabs } from 'nextra-theme-docs'
+
+# Mod Status
+
+## Overview
+
+The Mod Status API is part of the [Mod Setting](/apollo/developers/modules/modsetting) module and provides a simple way to retrieve the **current** value of any Lunar Client mod or setting within a mod for a player.
+
+- Request the player's current mod settings
+ - The response lists every mod option the player has changed from its default value, split into pages
+ - Read the value of any mod option afterwards, such as whether a mod is enabled or the minimap scale
+- Listen for updates
+ - Get notified whenever the value of a player's mod option changes, including changes made by server overrides
+
+
+ The Mod Status API is a **private feature** of the public Mod Setting module and not publicly available.
+ To use this feature, you must be granted access by the Lunar Client team. [Learn More](/apollo/developers/private-modules)
+
+
+## Integration
+
+### Sample Code
+
+Explore each integration by cycling through each tab, to find the best fit for your requirements and needs.
+
+
+
+
+
+
+**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers.
+
+
+**Request Mod Status**
+
+```java
+public void requestModStatusExample(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.requestModStatus(apolloPlayerOpt.get())
+ .onSuccess(response -> {
+ List options = response.getElements()
+ .stream().map(status -> status.getOption().getKey() + "=" + status.getValue())
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + options.size() + " changed options: " + options);
+ })
+ .onFailure(exception -> viewer.sendMessage("Failed to receive a response in time."));
+}
+```
+
+**Retrieve the current value for options**
+
+`getStatus` returns the values from the latest mod status response and any live updates since.
+
+```java
+private final ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
+
+private void printOptionStatusExample(Player player) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(player.getUniqueId());
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.requestModStatus(apolloPlayer)
+ .onSuccess(response -> {
+ // https://lunarclient.dev/apollo/developers/mods/waypoints#available-options
+ boolean waypointsEnabled = this.modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+
+ // https://lunarclient.dev/apollo/developers/mods/minimap#available-options
+ float minimapScale = this.modSettingModule.getStatus(apolloPlayer, ModMinimap.SCALE);
+
+ // https://lunarclient.dev/apollo/developers/mods/fov#available-options
+ int fovDefaultFov = this.modSettingModule.getStatus(apolloPlayer, ModFov.DEFAULT_FOV);
+
+ apolloPlayer.sendMessage(Component.text("Waypoints Enabled: ")
+ .append(Component.text(waypointsEnabled)));
+
+ apolloPlayer.sendMessage(Component.text("Minimap Scale: ")
+ .append(Component.text(minimapScale)));
+
+ apolloPlayer.sendMessage(Component.text("Fov Default Fov: ")
+ .append(Component.text(fovDefaultFov)));
+ })
+ .onFailure(exception -> apolloPlayer.sendMessage(Component.text("Failed to receive a response in time."))));
+}
+```
+
+**Listening for Updates**
+
+Apollo fires the [ApolloUpdateModOptionEvent](/apollo/developers/events) whenever a Lunar Client mod option is updated. It is not fired for the options listed in a mod status response.
+
+```java
+import com.lunarclient.apollo.event.ApolloListener;
+import com.lunarclient.apollo.event.EventBus;
+import com.lunarclient.apollo.event.Listen;
+import com.lunarclient.apollo.event.modsetting.ApolloUpdateModOptionEvent;
+import java.util.Objects;
+import net.kyori.adventure.text.Component;
+
+public class ApolloModStatusExample implements ApolloListener {
+
+ public ApolloModStatusExample() {
+ EventBus.getBus().register(this);
+ }
+
+ @Listen
+ private void onApolloUpdateModOption(ApolloUpdateModOptionEvent event) {
+ event.getPlayer().sendMessage(Component.text(event.getOption().getKey())
+ .append(Component.text(" was updated to "))
+ .append(Component.text(Objects.toString(event.getValue()))));
+ }
+
+}
+```
+
+
+
+
+
+
+**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup, and [Roundtrip Packets](/apollo/developers/lightweight/protobuf/roundtrip-packets) for `ApolloRoundtripProtoListener`.
+
+
+**Request Mod Status**
+
+```java
+public void requestModStatusExample(Player viewer) {
+ UUID requestId = UUID.randomUUID();
+
+ ModStatusRequest request = ModStatusRequest.newBuilder()
+ .setRequestId(ByteString.copyFromUtf8(requestId.toString()))
+ .build();
+
+ ApolloRoundtripProtoListener.getInstance().sendModStatusRequest(viewer, requestId, request)
+ .thenAccept(modOptions -> {
+ List keys = modOptions.stream()
+ .map(ModOptionStatus::getKey)
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + keys.size() + " changed options: " + keys);
+ }).exceptionally(throwable -> {
+ viewer.sendMessage("Failed to receive a response in time.");
+ return null;
+ });
+}
+```
+
+
+
+
+
+
+**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup, and [Roundtrip Packets](/apollo/developers/lightweight/json/roundtrip-packets) for `ApolloRoundtripJsonListener`.
+
+
+**Request Mod Status**
+
+```java
+public void requestModStatusExample(Player viewer) {
+ UUID requestId = UUID.randomUUID();
+
+ ApolloRoundtripJsonListener.getInstance()
+ .sendPaginatedRequest(viewer, requestId, new JsonObject(), "lunarclient.apollo.modsetting.v1.ModStatusRequest")
+ .thenAccept(options -> {
+ List keys = options.stream()
+ .map(option -> option.get("key").getAsString())
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + keys.size() + " changed options: " + keys);
+ }).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/mods/ApolloModStatusExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ApolloModStatusExample.java
index 71ef2a01..1a9a0f05 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ApolloModStatusExample.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ApolloModStatusExample.java
@@ -73,25 +73,27 @@ private void onBlockPlace(BlockPlaceEvent event) {
private void printOptionStatusExample(Player player) {
Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(player.getUniqueId());
- apolloPlayerOpt.ifPresent(apolloPlayer -> {
- // https://lunarclient.dev/apollo/developers/mods/waypoints#available-options
- boolean waypointsEnabled = this.modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.requestModStatus(apolloPlayer)
+ .onSuccess(response -> {
+ // https://lunarclient.dev/apollo/developers/mods/waypoints#available-options
+ boolean waypointsEnabled = this.modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
- // https://lunarclient.dev/apollo/developers/mods/minimap#available-options
- float minimapScale = this.modSettingModule.getStatus(apolloPlayer, ModMinimap.SCALE);
+ // https://lunarclient.dev/apollo/developers/mods/minimap#available-options
+ float minimapScale = this.modSettingModule.getStatus(apolloPlayer, ModMinimap.SCALE);
- // https://lunarclient.dev/apollo/developers/mods/fov#available-options
- int fovDefaultFov = this.modSettingModule.getStatus(apolloPlayer, ModFov.DEFAULT_FOV);
+ // https://lunarclient.dev/apollo/developers/mods/fov#available-options
+ int fovDefaultFov = this.modSettingModule.getStatus(apolloPlayer, ModFov.DEFAULT_FOV);
- apolloPlayer.sendMessage(Component.text("Waypoints Enabled: ")
- .append(Component.text(waypointsEnabled)));
+ apolloPlayer.sendMessage(Component.text("Waypoints Enabled: ")
+ .append(Component.text(waypointsEnabled)));
- apolloPlayer.sendMessage(Component.text("Minimap Scale: ")
- .append(Component.text(minimapScale)));
+ apolloPlayer.sendMessage(Component.text("Minimap Scale: ")
+ .append(Component.text(minimapScale)));
- apolloPlayer.sendMessage(Component.text("Fov Default Fov: ")
- .append(Component.text(fovDefaultFov)));
- });
+ apolloPlayer.sendMessage(Component.text("Fov Default Fov: ")
+ .append(Component.text(fovDefaultFov)));
+ })
+ .onFailure(exception -> apolloPlayer.sendMessage(Component.text("Failed to receive a response in time."))));
}
}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ModStatusCommand.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ModStatusCommand.java
index 39bed666..91d1ea55 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ModStatusCommand.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/mods/ModStatusCommand.java
@@ -25,10 +25,10 @@
import com.lunarclient.apollo.Apollo;
import com.lunarclient.apollo.ApolloManager;
+import com.lunarclient.apollo.module.modsetting.ModOptionStatus;
import com.lunarclient.apollo.module.modsetting.ModSettingModule;
import com.lunarclient.apollo.option.Option;
import com.lunarclient.apollo.player.ApolloPlayer;
-import java.util.Map;
import java.util.Optional;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@@ -86,20 +86,22 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
}
private void sendCurrent(CommandSender sender, ApolloPlayer target) {
- Map playerOptions = ApolloManager.getModsManager().getPlayerOptions().getPlayerOptions().get(target.getUniqueId());
-
- sender.sendMessage("-------------------------------------");
- sender.sendMessage("Target: " + target.getName());
- sender.sendMessage("");
-
- if (playerOptions != null && !playerOptions.isEmpty()) {
- for (Map.Entry entry : playerOptions.entrySet()) {
- sender.sendMessage(" - " + entry.getKey() + "=" + entry.getValue());
- }
- } else {
- sender.sendMessage("No options found!");
- }
-
- sender.sendMessage("-------------------------------------");
+ this.modSettingModule.requestModStatus(target)
+ .onSuccess(response -> {
+ sender.sendMessage("-------------------------------------");
+ sender.sendMessage("Target: " + target.getName());
+ sender.sendMessage("");
+
+ if (!response.getElements().isEmpty()) {
+ for (ModOptionStatus status : response.getElements()) {
+ sender.sendMessage(" - " + status.getOption().getKey() + "=" + status.getValue());
+ }
+ } else {
+ sender.sendMessage("No options found!");
+ }
+
+ sender.sendMessage("-------------------------------------");
+ })
+ .onFailure(exception -> sender.sendMessage("Failed to receive a response in time."));
}
}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java
index 016dd510..2e11b4d3 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java
@@ -39,6 +39,7 @@
import com.lunarclient.apollo.mods.impl.ModMinimap;
import com.lunarclient.apollo.mods.impl.ModWaypoints;
import com.lunarclient.apollo.module.inventory.InventoryModule;
+import com.lunarclient.apollo.module.modsetting.ModSettingModule;
import com.lunarclient.apollo.player.ApolloPlayer;
import java.util.HashSet;
import java.util.Optional;
@@ -55,6 +56,7 @@
public class InventoryApiExample extends InventoryExample implements ApolloListener, Listener {
private final InventoryModule inventoryModule = Apollo.getModuleManager().getModule(InventoryModule.class);
+ private final ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
private final Set minigameViewers = new HashSet<>();
@@ -77,6 +79,11 @@ public void displayHubLayoutExample(Player viewer) {
public void displayMinigameLayoutExample(Player viewer) {
this.minigameViewers.add(viewer.getUniqueId());
MinigameLayout.display(this.inventoryModule, viewer);
+
+ // Show Map / Waypoints follow the player's mod settings (private Mod Status API)
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> this.modSettingModule
+ .requestModStatus(apolloPlayer)
+ .onSuccess(response -> this.inventoryModule.displayInventoryButtons(apolloPlayer, MinigameLayout.modButtons(apolloPlayer))));
}
@Override
@@ -120,6 +127,7 @@ private void onPlayerQuit(PlayerQuitEvent event) {
}
// Update Minigame layout if the player toggles their Minimap or Waypoint mod
+ // ApolloUpdateModOptionEvent is part of the private Mod Status API: https://lunarclient.dev/apollo/developers/private-modules/modstatus
@Listen
private void onApolloUpdateModOption(ApolloUpdateModOptionEvent event) {
String optionKey = event.getOption().getKey();
@@ -127,15 +135,12 @@ private void onApolloUpdateModOption(ApolloUpdateModOptionEvent event) {
return;
}
- UUID playerIdentifier = event.getPlayer().getUniqueId();
- if (!this.minigameViewers.contains(playerIdentifier)) {
+ ApolloPlayer apolloPlayer = event.getPlayer();
+ if (!this.minigameViewers.contains(apolloPlayer.getUniqueId())) {
return;
}
- Player viewer = Bukkit.getPlayer(playerIdentifier);
- if (viewer != null) {
- MinigameLayout.display(this.inventoryModule, viewer);
- }
+ this.inventoryModule.displayInventoryButtons(apolloPlayer, MinigameLayout.modButtons(apolloPlayer));
}
}
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 76caec20..309e327f 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
@@ -79,4 +79,24 @@ public void requestInstalledModsExample(Player viewer) {
});
}
+ @Override
+ public void requestModStatusExample(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.requestModStatus(apolloPlayerOpt.get())
+ .onSuccess(response -> {
+ List options = response.getElements()
+ .stream().map(status -> status.getOption().getKey() + "=" + status.getValue())
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + options.size() + " changed options: " + options);
+ })
+ .onFailure(exception -> viewer.sendMessage("Failed to receive a response in time."));
+ }
+
}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java
index dc1633db..231b6448 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java
@@ -42,7 +42,9 @@
import com.lunarclient.apollo.player.ApolloPlayer;
import java.awt.Color;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.UUID;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@@ -160,79 +162,86 @@ public static void display(InventoryModule inventoryModule, Player viewer) {
.onClick(ApolloButtonAction.runCommand("/settings"))
.build();
- ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
- boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED);
- boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
-
- InventoryButton.InventoryButtonBuilder, ?> showMapBuilder = InventoryButton.builder()
- .id("show-map")
- .inventoryType(InventoryType.PLAYER)
- .box(InventoryButtonBox.RIGHT)
- .position(HudPosition.of(6, 52))
- .size(InventoryButton.SIZE_WIDE)
- .shape(ApolloButtonShape.ROUNDED_SQUARE)
- .content(ApolloButtonContent.builder()
- .append(ItemStackIcon.builder()
- .itemName("FILLED_MAP")
- .build())
- .append(Component.text("Show Map"))
- .scale(1.0F)
- .build());
-
- if (minimapEnabled) {
- showMapBuilder
- .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
- .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
- .tooltip(ApolloButtonTooltip.of(
- Component.text("Show Map", NamedTextColor.AQUA),
- Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY)))
- .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW));
- } else {
- showMapBuilder
- .backgroundColor(new Color(224, 64, 64, 128))
- .borderColor(new Color(255, 200, 200, 140))
- .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED)));
- }
-
- InventoryButton showMap = showMapBuilder.build();
-
- InventoryButton.InventoryButtonBuilder, ?> waypointsBuilder = InventoryButton.builder()
- .id("waypoints")
- .inventoryType(InventoryType.PLAYER)
- .box(InventoryButtonBox.RIGHT)
- .position(HudPosition.of(6, 84))
- .size(InventoryButton.SIZE_WIDE)
- .shape(ApolloButtonShape.ROUNDED_SQUARE)
- .content(ApolloButtonContent.builder()
- .append(ItemStackIcon.builder()
- .itemName("LODESTONE")
- .build())
- .append(Component.text("Waypoints"))
- .scale(1.0F)
- .build());
-
- if (waypointsEnabled) {
- waypointsBuilder
- .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
- .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
- .tooltip(ApolloButtonTooltip.of(
- Component.text("Waypoints", NamedTextColor.GOLD),
- Component.text("Manage your waypoints", NamedTextColor.GRAY)))
- .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU));
- } else {
- waypointsBuilder
- .backgroundColor(new Color(224, 64, 64, 128))
- .borderColor(new Color(255, 200, 200, 140))
- .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED)));
- }
-
- InventoryButton waypoints = waypointsBuilder.build();
-
- inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(mapInfo, kills, lobby,
- profile, settings, showMap, waypoints));
+ List buttons = new ArrayList<>(Arrays.asList(mapInfo, kills, lobby, profile, settings));
+ buttons.addAll(modButtons(apolloPlayer));
+ inventoryModule.displayInventoryButtons(apolloPlayer, buttons);
});
}
+ // Displaying these again with the same ids replaces the previous Show Map / Waypoints buttons
+ public static List modButtons(ApolloPlayer apolloPlayer) {
+ // getStatus is part of the private Mod Status API: https://lunarclient.dev/apollo/developers/private-modules/modstatus
+ ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
+ boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED);
+ boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+
+ InventoryButton.InventoryButtonBuilder, ?> showMapBuilder = InventoryButton.builder()
+ .id("show-map")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 52))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("FILLED_MAP")
+ .build())
+ .append(Component.text("Show Map"))
+ .scale(1.0F)
+ .build());
+
+ if (minimapEnabled) {
+ showMapBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Show Map", NamedTextColor.AQUA),
+ Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW));
+ } else {
+ showMapBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED)));
+ }
+
+ InventoryButton showMap = showMapBuilder.build();
+
+ InventoryButton.InventoryButtonBuilder, ?> waypointsBuilder = InventoryButton.builder()
+ .id("waypoints")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 84))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("LODESTONE")
+ .build())
+ .append(Component.text("Waypoints"))
+ .scale(1.0F)
+ .build());
+
+ if (waypointsEnabled) {
+ waypointsBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Waypoints", NamedTextColor.GOLD),
+ Component.text("Manage your waypoints", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU));
+ } else {
+ waypointsBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED)));
+ }
+
+ InventoryButton waypoints = waypointsBuilder.build();
+
+ return Arrays.asList(showMap, waypoints);
+ }
+
private static int getKills(ApolloPlayer apolloViewer) {
Player player = Bukkit.getPlayer(apolloViewer.getUniqueId());
return player != null ? player.getStatistic(Statistic.PLAYER_KILLS) : 0;
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 b62551fa..104ce09e 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,7 +44,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
if (args.length != 1) {
player.sendMessage("Usage: /modsettings ");
- player.sendMessage("Usage: /modsettings ");
+ player.sendMessage("Usage: /modsettings ");
return true;
}
@@ -75,9 +75,15 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
break;
}
+ case "requestmodstatus": {
+ modSettingsExample.requestModStatusExample(player);
+ player.sendMessage("Requesting mod status.....");
+ break;
+ }
+
default: {
player.sendMessage("Usage: /modsettings ");
- 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 71777e2d..ae5b6d7d 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
@@ -36,4 +36,6 @@ public abstract class ModSettingsExample extends ApolloModuleExample {
public abstract void requestInstalledModsExample(Player viewer);
+ public abstract void requestModStatusExample(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 1c2ff26f..518c3a0e 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
@@ -88,6 +88,9 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla
} else if ("lunarclient.apollo.modsetting.v1.InstalledModsResponse".equals(type)) {
UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
this.parseModGroups(requestId, payload);
+ } else if ("lunarclient.apollo.modsetting.v1.ModStatusResponse".equals(type)) {
+ UUID requestId = UUID.fromString(payload.get("request_id").getAsString().replace("+", "-"));
+ this.parseModStatus(requestId, payload);
}
}
@@ -155,6 +158,19 @@ private void parseModGroups(UUID requestId, JsonObject response) {
this.handlePage(requestId, accumulated, response);
}
+ private void parseModStatus(UUID requestId, JsonObject response) {
+ List accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
+
+ JsonArray modOptions = response.getAsJsonArray("mod_options");
+ if (modOptions != null) {
+ for (JsonElement option : modOptions) {
+ accumulated.add(option.getAsJsonObject());
+ }
+ }
+
+ 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;
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 d260febf..fc1fb4a8 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
@@ -83,4 +83,22 @@ public void requestInstalledModsExample(Player viewer) {
});
}
+ @Override
+ public void requestModStatusExample(Player viewer) {
+ UUID requestId = UUID.randomUUID();
+
+ ApolloRoundtripJsonListener.getInstance()
+ .sendPaginatedRequest(viewer, requestId, new JsonObject(), "lunarclient.apollo.modsetting.v1.ModStatusRequest")
+ .thenAccept(options -> {
+ List keys = options.stream()
+ .map(option -> option.get("key").getAsString())
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + keys.size() + " changed options: " + keys);
+ }).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 03d43c72..903aa566 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
@@ -30,6 +30,9 @@
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.modsetting.v1.ModOptionStatus;
+import com.lunarclient.apollo.modsetting.v1.ModStatusRequest;
+import com.lunarclient.apollo.modsetting.v1.ModStatusResponse;
import com.lunarclient.apollo.transfer.v1.PingResponse;
import com.lunarclient.apollo.transfer.v1.TransferResponse;
import java.util.ArrayList;
@@ -56,6 +59,8 @@ public class ApolloRoundtripProtoListener implements PluginMessageListener {
private final Map>> roundTripPacketFutures = new ConcurrentHashMap<>();
private final Map>> paginatedFutures = new ConcurrentHashMap<>();
private final Map> paginatedAccumulator = new ConcurrentHashMap<>();
+ private final Map>> modStatusFutures = new ConcurrentHashMap<>();
+ private final Map> modStatusAccumulator = new ConcurrentHashMap<>();
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
public ApolloRoundtripProtoListener(ApolloExamplePlugin plugin) {
@@ -79,6 +84,10 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla
InstalledModsResponse message = any.unpack(InstalledModsResponse.class);
UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8());
this.handlePagedResponse(requestId, message);
+ } else if (any.is(ModStatusResponse.class)) {
+ ModStatusResponse message = any.unpack(ModStatusResponse.class);
+ UUID requestId = UUID.fromString(message.getRequestId().toStringUtf8());
+ this.handleModStatusPage(requestId, message);
}
} catch (InvalidProtocolBufferException e) {
@@ -125,6 +134,38 @@ public CompletableFuture> sendPaginatedRequest(Player player, UUID req
return future;
}
+ public CompletableFuture> sendModStatusRequest(Player player, UUID requestId, ModStatusRequest request) {
+ ProtobufPacketUtil.sendPacket(player, request);
+
+ CompletableFuture> future = new CompletableFuture<>();
+ this.modStatusFutures.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.modStatusAccumulator.remove(requestId);
+ });
+
+ return future;
+ }
+
+ private void handleModStatusPage(UUID requestId, ModStatusResponse response) {
+ List accumulated = this.modStatusAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
+ accumulated.addAll(response.getModOptionsList());
+
+ if (response.getPage() == response.getTotalPages() - 1) {
+ this.modStatusAccumulator.remove(requestId);
+ CompletableFuture> future = this.modStatusFutures.remove(requestId);
+ if (future != null) {
+ future.complete(accumulated);
+ }
+ }
+ }
+
private void handlePagedResponse(UUID requestId, InstalledModsResponse response) {
List accumulated = this.paginatedAccumulator.computeIfAbsent(requestId, k -> new ArrayList<>());
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 04780af3..94dc6798 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
@@ -32,6 +32,8 @@
import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil;
import com.lunarclient.apollo.modsetting.v1.InstalledModsRequest;
import com.lunarclient.apollo.modsetting.v1.Mod;
+import com.lunarclient.apollo.modsetting.v1.ModOptionStatus;
+import com.lunarclient.apollo.modsetting.v1.ModStatusRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -90,4 +92,25 @@ public void requestInstalledModsExample(Player viewer) {
});
}
+ @Override
+ public void requestModStatusExample(Player viewer) {
+ UUID requestId = UUID.randomUUID();
+
+ ModStatusRequest request = ModStatusRequest.newBuilder()
+ .setRequestId(ByteString.copyFromUtf8(requestId.toString()))
+ .build();
+
+ ApolloRoundtripProtoListener.getInstance().sendModStatusRequest(viewer, requestId, request)
+ .thenAccept(modOptions -> {
+ List keys = modOptions.stream()
+ .map(ModOptionStatus::getKey)
+ .collect(Collectors.toList());
+
+ viewer.sendMessage("Found " + keys.size() + " changed options: " + keys);
+ }).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 ba4c1777..4ed5c3ef 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.2.5"
+protobuf = "0.2.6"
gson = "2.10.1"
shadow = "9.4.1"
spotless = "8.4.0"