diff --git a/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java
new file mode 100644
index 00000000..78035f57
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java
@@ -0,0 +1,70 @@
+/*
+ * 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.heightlimit;
+
+import lombok.Builder;
+import lombok.Getter;
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Range;
+
+/**
+ * Represents a height limit which can be shown on the client.
+ *
+ * @since 1.2.9
+ */
+@Getter
+@Builder
+public final class HeightLimit {
+
+ /**
+ * Returns the height limit {@link String} world name.
+ *
+ * @return the height limit world name
+ * @since 1.2.9
+ */
+ String world;
+
+ /**
+ * Returns the height limit {@link Integer} Y level where block placement
+ * is denied.
+ *
+ *
The highest buildable layer is {@code limit - 1}.
+ *
+ * @return the height limit
+ * @since 1.2.9
+ */
+ @Range(from = 1, to = Integer.MAX_VALUE) int limit;
+
+ /**
+ * Returns the height limit {@link Component} display name.
+ *
+ * Shown on the client's height limit HUD.
+ *
+ * @return the height limit display name
+ * @since 1.2.9
+ */
+ @Nullable Component displayName;
+
+}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java
new file mode 100644
index 00000000..86431321
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java
@@ -0,0 +1,118 @@
+/*
+ * 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.heightlimit;
+
+import com.lunarclient.apollo.module.ApolloModule;
+import com.lunarclient.apollo.module.ModuleDefinition;
+import com.lunarclient.apollo.option.ListOption;
+import com.lunarclient.apollo.option.Option;
+import com.lunarclient.apollo.recipients.Recipients;
+import io.leangen.geantyref.TypeToken;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Represents the height limit module.
+ *
+ * Sets the build-height limit shown by the clients Height Limit mod
+ * (block overlay and HUD display). The client resolves the active limit
+ * against the world the player is currently in.
+ *
+ * This module only provides a visual indicator for the player. The
+ * server is still responsible for cancelling block placement above the
+ * height limit.
+ *
+ * @since 1.2.9
+ */
+@ApiStatus.NonExtendable
+@ModuleDefinition(id = "height_limit", name = "Height Limit")
+public abstract class HeightLimitModule extends ApolloModule {
+
+ private static final HeightLimit OVERWORLD_HEIGHT_LIMIT = HeightLimit.builder()
+ .world("world")
+ .limit(200)
+ .displayName(Component.text("Overworld", NamedTextColor.GOLD))
+ .build();
+
+ /**
+ * Returns the default list of height limits to send to the player.
+ *
+ * @since 1.2.9
+ */
+ public static final ListOption DEFAULT_HEIGHT_LIMITS = Option.list()
+ .comment("Sets the default height limits to send to the player.")
+ .node("default-height-limits").type(new TypeToken>() {})
+ .defaultValue(new ArrayList<>(Collections.singletonList(HeightLimitModule.OVERWORLD_HEIGHT_LIMIT)))
+ .build();
+
+ protected HeightLimitModule() {
+ this.registerOptions(
+ ApolloModule.ENABLE_OPTION_OFF,
+ HeightLimitModule.DEFAULT_HEIGHT_LIMITS
+ );
+ }
+
+ /**
+ * Overrides the {@link HeightLimit} for the {@link Recipients}.
+ *
+ * Sending a height limit for an already-known world replaces
+ * that worlds entry.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param heightLimit the height limit
+ * @since 1.2.9
+ */
+ public abstract void overrideHeightLimit(Recipients recipients, HeightLimit heightLimit);
+
+ /**
+ * Removes the {@link HeightLimit} from the {@link Recipients}.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param world the world name
+ * @since 1.2.9
+ */
+ public abstract void removeHeightLimit(Recipients recipients, String world);
+
+ /**
+ * Removes the {@link HeightLimit} from the {@link Recipients}.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param heightLimit the height limit
+ * @since 1.2.9
+ */
+ public abstract void removeHeightLimit(Recipients recipients, HeightLimit heightLimit);
+
+ /**
+ * Resets all {@link HeightLimit}s for the {@link Recipients}.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @since 1.2.9
+ */
+ public abstract void resetHeightLimits(Recipients recipients);
+
+}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java b/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java
index 27640400..3b305bd3 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java
@@ -45,4 +45,15 @@ public final class PingRequest extends ApolloRequest {
*/
List serverIps;
+ /**
+ * Returns the timeout for ping requests, in milliseconds.
+ *
+ * @return the request timeout, in milliseconds
+ * @since 1.2.9
+ */
+ @Override
+ public long getTimeoutMillis() {
+ return 10_000L;
+ }
+
}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java b/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java
index 2d033f4d..39b08ed8 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java
@@ -42,6 +42,14 @@
@ModuleDefinition(id = "transfer", name = "Transfer")
public abstract class TransferModule extends ApolloModule {
+ /**
+ * The maximum amount of server IPs the client will ping
+ * for a single {@link PingRequest}.
+ *
+ * @since 1.2.9
+ */
+ public static final int MAX_PINGS_PER_PACKET = 10;
+
@Override
public Collection getSupportedPlatforms() {
return Arrays.asList(ApolloPlatform.Kind.SERVER, ApolloPlatform.Kind.PROXY);
@@ -55,6 +63,8 @@ public Collection getSupportedPlatforms() {
* @param player the player
* @param serverIps all server IPs to ping
* @return future to be listened to for errors/success
+ * @throws IllegalArgumentException if no server IPs or more than
+ * {@value #MAX_PINGS_PER_PACKET} server IPs are provided
* @since 1.0.0
*/
public Future ping(ApolloPlayer player, List serverIps) {
@@ -85,6 +95,8 @@ public Future transfer(ApolloPlayer player, String serverIp) {
* @param player the player
* @param request the ping request
* @return future to be listened to for errors/success
+ * @throws IllegalArgumentException if no server IPs or more than
+ * {@value #MAX_PINGS_PER_PACKET} server IPs are provided
* @since 1.0.0
*/
public abstract Future ping(ApolloPlayer player, PingRequest request);
diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java
index 372d8ae3..f5bb25b9 100644
--- a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java
+++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java
@@ -67,4 +67,14 @@ public ApolloRequest() {
this.sentTime = System.currentTimeMillis();
}
+ /**
+ * Returns the time to wait for a response, in milliseconds.
+ *
+ * @return the request timeout, in milliseconds
+ * @since 1.2.9
+ */
+ public long getTimeoutMillis() {
+ return TIMEOUT;
+ }
+
}
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 f0d5bce8..31771c4c 100644
--- a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java
+++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java
@@ -119,13 +119,14 @@ public void registerListener(ApolloRequest request
this.paginationManager.handleTimeout(packetId);
if (listener != null) {
- Throwable error = new Throwable("Timeout exceeded!");
+ Throwable error = new Throwable("Timeout exceeded! No " + request.getClass().getSimpleName()
+ + " response received within " + request.getTimeoutMillis() + "ms");
future.handleFailure(error);
}
} catch (Exception e) {
e.printStackTrace();
}
- }, ApolloRequest.TIMEOUT, TimeUnit.MILLISECONDS);
+ }, request.getTimeoutMillis(), TimeUnit.MILLISECONDS);
this.listeners.put(packetId, (UncertainFuture) future);
}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java
new file mode 100644
index 00000000..e8887aed
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java
@@ -0,0 +1,156 @@
+/*
+ * 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.heightlimit;
+
+import com.lunarclient.apollo.ApolloManager;
+import com.lunarclient.apollo.common.ApolloComponent;
+import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent;
+import com.lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage;
+import com.lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage;
+import com.lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage;
+import com.lunarclient.apollo.option.config.Serializer;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import com.lunarclient.apollo.recipients.Recipients;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.List;
+import lombok.NonNull;
+import net.kyori.adventure.text.Component;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import static com.lunarclient.apollo.util.Ranges.checkStrictlyPositive;
+
+/**
+ * Provides the height limit module.
+ *
+ * @since 1.2.9
+ */
+public final class HeightLimitModuleImpl extends HeightLimitModule implements Serializer {
+
+ /**
+ * Creates a new instance of {@link HeightLimitModuleImpl}.
+ *
+ * @since 1.2.9
+ */
+ public HeightLimitModuleImpl() {
+ super();
+ this.serializer(HeightLimit.class, new HeightLimitSerializer());
+ this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister);
+ }
+
+ @Override
+ public void overrideHeightLimit(@NonNull Recipients recipients, @NonNull HeightLimit heightLimit) {
+ OverrideHeightLimitMessage.Builder builder = OverrideHeightLimitMessage.newBuilder()
+ .setWorld(heightLimit.getWorld())
+ .setLimit(checkStrictlyPositive(heightLimit.getLimit(), "HeightLimit#limit"));
+
+ Component displayName = heightLimit.getDisplayName();
+ if (displayName != null) {
+ builder.setDisplayNameAdventureJsonLines(ApolloComponent.toJson(displayName));
+ }
+
+ OverrideHeightLimitMessage message = builder.build();
+ ApolloManager.getNetworkManager().sendPacket(recipients, message);
+ }
+
+ @Override
+ public void removeHeightLimit(@NonNull Recipients recipients, @NonNull String world) {
+ RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder()
+ .setWorld(world)
+ .build();
+
+ ApolloManager.getNetworkManager().sendPacket(recipients, message);
+ }
+
+ @Override
+ public void removeHeightLimit(@NonNull Recipients recipients, @NonNull HeightLimit heightLimit) {
+ this.removeHeightLimit(recipients, heightLimit.getWorld());
+ }
+
+ @Override
+ public void resetHeightLimits(@NonNull Recipients recipients) {
+ ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance();
+ ApolloManager.getNetworkManager().sendPacket(recipients, message);
+ }
+
+ private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
+ ApolloPlayer player = event.getPlayer();
+ List heightLimits = this.getOptions().get(player, HeightLimitModule.DEFAULT_HEIGHT_LIMITS);
+
+ if (heightLimits != null) {
+ for (HeightLimit heightLimit : heightLimits) {
+ this.overrideHeightLimit(player, heightLimit);
+ }
+ }
+ }
+
+ private static final class HeightLimitSerializer implements TypeSerializer {
+
+ @Override
+ public HeightLimit deserialize(Type type, ConfigurationNode node) throws SerializationException {
+ HeightLimit.HeightLimitBuilder builder = HeightLimit.builder()
+ .world(this.virtualNode(node, "world").getString())
+ .limit(this.virtualNode(node, "limit").getInt());
+
+ String displayName = node.node("display-name").getString();
+ if (displayName != null) {
+ builder.displayName(ApolloComponent.fromLegacyAmpersand(displayName));
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public void serialize(Type type, @Nullable HeightLimit heightLimit, ConfigurationNode node) throws SerializationException {
+ if (heightLimit == null) {
+ node.raw(null);
+ return;
+ }
+
+ node.node("world").set(heightLimit.getWorld());
+ node.node("limit").set(heightLimit.getLimit());
+
+ Component displayName = heightLimit.getDisplayName();
+ if (displayName != null) {
+ node.node("display-name").set(ApolloComponent.toLegacyAmpersand(displayName));
+ }
+ }
+
+ private ConfigurationNode virtualNode(ConfigurationNode source, Object... path) throws SerializationException {
+ if (!source.hasChild(path)) {
+ throw new SerializationException("Required field " + Arrays.toString(path) + " not found!");
+ }
+
+ return source.node(path);
+ }
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java
index 1446ef11..cbc97f0d 100644
--- a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java
+++ b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java
@@ -56,6 +56,10 @@ public PacketEnrichmentImpl() {
}
private void onReceivePacket(ApolloReceivePacketEvent event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
Options options = this.getOptions();
if (options.get(PacketEnrichmentModule.PLAYER_ATTACK_EVENT)) {
diff --git a/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java
index 6971cf56..febd0109 100644
--- a/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java
+++ b/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java
@@ -53,9 +53,20 @@ public TransferModuleImpl() {
@Override
public Future ping(@NonNull ApolloPlayer player, @NonNull PingRequest request) {
+ List serverIps = request.getServerIps();
+
+ if (serverIps == null || serverIps.isEmpty()) {
+ throw new IllegalArgumentException("PingRequest must contain at least 1 server IP!");
+ }
+
+ if (serverIps.size() > MAX_PINGS_PER_PACKET) {
+ throw new IllegalArgumentException("PingRequest supports up to " + MAX_PINGS_PER_PACKET
+ + " server IPs, got " + serverIps.size() + "!");
+ }
+
com.lunarclient.apollo.transfer.v1.PingRequest requestProto = com.lunarclient.apollo.transfer.v1.PingRequest.newBuilder()
.setRequestId(ByteString.copyFromUtf8(request.getRequestId().toString()))
- .addAllServerIps(request.getServerIps())
+ .addAllServerIps(serverIps)
.build();
return ((AbstractApolloPlayer) player).sendRoundTripPacket(request, requestProto);
diff --git a/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java
index b9ed8e3b..320eab67 100644
--- a/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java
+++ b/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java
@@ -110,6 +110,10 @@ public void hideWaypoint(@NonNull Recipients recipients, @NonNull String waypoin
}
private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
+ if (!this.isEnabled()) {
+ return;
+ }
+
ApolloPlayer player = event.getPlayer();
List waypoints = this.getOptions().get(player, WaypointModule.DEFAULT_WAYPOINTS);
diff --git a/docs/developers/lightweight/protobuf.mdx b/docs/developers/lightweight/protobuf.mdx
index f39b9e45..2759440a 100644
--- a/docs/developers/lightweight/protobuf.mdx
+++ b/docs/developers/lightweight/protobuf.mdx
@@ -26,7 +26,7 @@ Available fields for each message, including their types, are available on the B
com.lunarclient
apollo-protos
- 0.2.0
+ 0.2.1
```
@@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B
}
dependencies {
- api 'com.lunarclient:apollo-protos:0.2.0'
+ api 'com.lunarclient:apollo-protos:0.2.1'
}
```
@@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B
}
dependencies {
- api("com.lunarclient:apollo-protos:0.2.0")
+ api("com.lunarclient:apollo-protos:0.2.1")
}
```
diff --git a/docs/developers/modules.mdx b/docs/developers/modules.mdx
index 16803d54..1d8f8fc7 100644
--- a/docs/developers/modules.mdx
+++ b/docs/developers/modules.mdx
@@ -16,9 +16,11 @@ These modules are available to all servers using Apollo and do not require any p
🔗 [Entity](/apollo/developers/modules/entity)
🔗 [Glint](/apollo/developers/modules/glint)
🔗 [Glow](/apollo/developers/modules/glow)
+🔗 [Height Limit](/apollo/developers/modules/heightlimit)
🔗 [Hologram](/apollo/developers/modules/hologram)
🔗 [Inventory](/apollo/developers/modules/inventory)
🔗 [Limb](/apollo/developers/modules/limb)
+🔗 [Marker](/apollo/developers/modules/marker)
🔗 [Mod Setting](/apollo/developers/modules/modsetting)
🔗 [Nametag](/apollo/developers/modules/nametag)
🔗 [Nick Hider](/apollo/developers/modules/nickhider)
diff --git a/docs/developers/modules/_meta.json b/docs/developers/modules/_meta.json
index 182a8a35..11e872d0 100644
--- a/docs/developers/modules/_meta.json
+++ b/docs/developers/modules/_meta.json
@@ -9,6 +9,7 @@
"entity": "Entity",
"glint": "Glint",
"glow": "Glow",
+ "heightlimit": "Height Limit",
"hologram": "Hologram",
"inventory": "Inventory",
"limb": "Limb",
diff --git a/docs/developers/modules/heightlimit.mdx b/docs/developers/modules/heightlimit.mdx
new file mode 100644
index 00000000..b22148dd
--- /dev/null
+++ b/docs/developers/modules/heightlimit.mdx
@@ -0,0 +1,202 @@
+import { Callout, Tab, Tabs } from 'nextra-theme-docs'
+
+# Height Limit Module
+
+## Overview
+
+The height limit module allows servers to control the build-height limit displayed by the Height Limit mod.
+
+- Adds the ability to set a height limit per world, rendered as a block overlay and HUD display.
+
+
+ This module is disabled by default, if you wish to use this module you will need to enable it in `config.yml`.
+
+
+
+ This module only provides a visual indicator for the player, the server is still responsible for cancelling block placement above the height limit.
+
+
+## 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.
+
+
+### Overriding a Height Limit
+
+```java
+public void overrideHeightLimitExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+
+ apolloPlayerOpt.ifPresent(apolloPlayer -> {
+ this.heightLimitModule.overrideHeightLimit(apolloPlayer, HeightLimit.builder()
+ .world("world_the_end")
+ .limit(150)
+ .displayName(Component.text("The End", NamedTextColor.DARK_PURPLE))
+ .build()
+ );
+ });
+}
+```
+
+### Removing a Height Limit
+
+```java
+public void removeHeightLimitExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.heightLimitModule.removeHeightLimit(apolloPlayer, "world_the_end"));
+}
+```
+
+### Resetting all Height Limits
+
+```java
+public void resetHeightLimitsExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(this.heightLimitModule::resetHeightLimits);
+}
+```
+
+### `HeightLimit` Options
+
+`.world(String)` is the world, by name, that you wish to apply the height limit to. Sending a height limit for an already-known world replaces that worlds entry.
+
+```java
+.world("world_the_end")
+```
+
+`.limit(Integer)` is the Y level where block placement is denied. The highest buildable layer is `limit - 1`.
+
+```java
+.limit(150)
+```
+
+`.displayName(Component)` is the optional display name shown on the clients height limit HUD.
+
+```java
+.displayName(Component.text("The End", NamedTextColor.DARK_PURPLE))
+```
+
+
+
+
+
+
+**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup.
+
+
+
+ Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/protobuf/player-detection) example.
+
+
+**Overriding a Height Limit**
+
+```java
+public void overrideHeightLimitExample(Player viewer) {
+ OverrideHeightLimitMessage message = OverrideHeightLimitMessage.newBuilder()
+ .setWorld("world_the_end")
+ .setLimit(150)
+ .setDisplayNameAdventureJsonLines(AdventureUtil.toJson(
+ Component.text("The End", NamedTextColor.DARK_PURPLE)
+ ))
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+```
+
+**Removing a Height Limit**
+
+```java
+public void removeHeightLimitExample(Player viewer) {
+ RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder()
+ .setWorld("world_the_end")
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+```
+
+**Resetting all Height Limits**
+
+```java
+public void resetHeightLimitsExample(Player viewer) {
+ ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance();
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+```
+
+
+
+
+
+
+**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup.
+
+
+
+ Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/json/player-detection) example.
+
+
+**Overriding a Height Limit**
+
+```java
+public void overrideHeightLimitExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage");
+ message.addProperty("world", "world_the_end");
+ message.addProperty("limit", 150);
+ message.addProperty("display_name_adventure_json_lines", AdventureUtil.toJson(
+ Component.text("The End", NamedTextColor.DARK_PURPLE)
+ ));
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+```
+
+**Removing a Height Limit**
+
+```java
+public void removeHeightLimitExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage");
+ message.addProperty("world", "world_the_end");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+```
+
+**Resetting all Height Limits**
+
+```java
+public void resetHeightLimitsExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+```
+
+
+
+
+
+## Available options
+
+- __`DEFAULT_HEIGHT_LIMITS`__
+ - Sets the default height limits to send to the player.
+ - Values
+ - Type: `List`
+ - Default:
+ ```yaml
+ - world: world
+ limit: 200
+ display-name: '&6Overworld'
+ ```
diff --git a/docs/developers/modules/inventory.mdx b/docs/developers/modules/inventory.mdx
index 35615fe2..62b9a65a 100644
--- a/docs/developers/modules/inventory.mdx
+++ b/docs/developers/modules/inventory.mdx
@@ -34,27 +34,27 @@ Explore each integration by cycling through each tab, to find the best fit for y
**Copy To Clipboard Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:paper",Count:1b,components:{"minecraft:custom_name":"COPY TO CLIPBOARD","minecraft:custom_data":{lunar:{unclickable:true,copyToClipboard:"lunarclient.com"}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:paper",Count:1b,components:{"minecraft:custom_name":"COPY TO CLIPBOARD","minecraft:custom_data":{lunar:{unclickable:true,copy_to_clipboard:"lunarclient.com"}}}}}`
**Open URL Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:torch",Count:1b,components:{"minecraft:custom_name":"OPEN URL","minecraft:custom_data":{lunar:{unclickable:true,openUrl:"https://lunarclient.com"}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:torch",Count:1b,components:{"minecraft:custom_name":"OPEN URL","minecraft:custom_data":{lunar:{unclickable:true,open_url:"https://lunarclient.com"}}}}}`
**Suggest Command Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:book",Count:1b,components:{"minecraft:custom_name":"SUGGEST COMMAND","minecraft:custom_data":{lunar:{unclickable:true,suggestCommand:"/apollo"}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:book",Count:1b,components:{"minecraft:custom_name":"SUGGEST COMMAND","minecraft:custom_data":{lunar:{unclickable:true,suggest_command:"/apollo"}}}}}`
**Run Command Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:writable_book",Count:1b,components:{"minecraft:custom_name":"RUN COMMAND","minecraft:custom_data":{lunar:{unclickable:true,runCommand:"/apollo"}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:writable_book",Count:1b,components:{"minecraft:custom_name":"RUN COMMAND","minecraft:custom_data":{lunar:{unclickable:true,run_command:"/apollo"}}}}}`
**Hide Item Tooltip Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:sponge",Count:1b,components:{"minecraft:custom_name":"HIDE ITEM TOOLTIP","minecraft:custom_data":{lunar:{unclickable:true,hideItemTooltip:true}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:sponge",Count:1b,components:{"minecraft:custom_name":"HIDE ITEM TOOLTIP","minecraft:custom_data":{lunar:{unclickable:true,hide_item_tooltip:true}}}}}`
**Hide Slot Highlight Item**
-`/summon item ~ ~1 ~ {Item:{id:"minecraft:dirt",Count:1b,components:{"minecraft:custom_name":"HIDE SLOT HIGHTLIGHT","minecraft:custom_data":{lunar:{unclickable:true,hideSlotHighlight:true}}}}}`
+`/summon item ~ ~1 ~ {Item:{id:"minecraft:dirt",Count:1b,components:{"minecraft:custom_name":"HIDE SLOT HIGHTLIGHT","minecraft:custom_data":{lunar:{unclickable:true,hide_slot_highlight:true}}}}}`
diff --git a/docs/developers/modules/transfer.mdx b/docs/developers/modules/transfer.mdx
index b2ea55b0..b099d973 100644
--- a/docs/developers/modules/transfer.mdx
+++ b/docs/developers/modules/transfer.mdx
@@ -119,6 +119,7 @@ public void transferExample(Player viewer) {
You can provide up to `10` different addresses per ping packet.
+ Requests with more addresses are rejected with an `IllegalArgumentException`.
@@ -209,6 +210,7 @@ public void transferExample(Player player) {
You can provide up to `10` different addresses per ping packet.
+ Addresses beyond the first `10` are reported as `STATUS_TIMED_OUT` instead of being pinged.
```java
diff --git a/docs/developers/modules/waypoint.mdx b/docs/developers/modules/waypoint.mdx
index d693cd68..78642822 100644
--- a/docs/developers/modules/waypoint.mdx
+++ b/docs/developers/modules/waypoint.mdx
@@ -433,7 +433,18 @@ public void resetWaypointsExample(Player viewer) {
- Sets the default waypoints to send to the player.
- Values
- Type: `List`
- - Default: `Empty List`
+ - Default:
+ ```yaml
+ - name: Spawn
+ location:
+ world: world
+ x: 0
+ y: 100
+ z: 0
+ color: '#FF0000'
+ prevent-removal: false
+ hidden: false
+ ```
## Automatic Waypoint Creation from Chat
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java
index 2ae26a66..ffdc2396 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java
@@ -40,6 +40,7 @@
import com.lunarclient.apollo.example.api.module.CosmeticApiExample;
import com.lunarclient.apollo.example.api.module.EntityApiExample;
import com.lunarclient.apollo.example.api.module.GlowApiExample;
+import com.lunarclient.apollo.example.api.module.HeightLimitApiExample;
import com.lunarclient.apollo.example.api.module.HologramApiExample;
import com.lunarclient.apollo.example.api.module.LimbApiExample;
import com.lunarclient.apollo.example.api.module.MarkerApiExample;
@@ -93,6 +94,7 @@ public void registerModuleExamples() {
this.setCooldownExample(new CooldownApiExample());
this.setEntityExample(new EntityApiExample());
this.setGlowExample(new GlowApiExample());
+ this.setHeightLimitExample(new HeightLimitApiExample());
this.setHologramExample(new HologramApiExample());
this.setLimbExample(new LimbApiExample());
this.setMarkerExample(new MarkerApiExample());
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java
index d0cb1938..068ff039 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java
@@ -82,6 +82,11 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) {
for (CommandCosmetic spec : npc.getCosmetics()) {
cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec);
}
+
+ PlayerNpc.ActiveEmote emote = npc.getActiveEmote();
+ if (emote != null && npc.getViewers().contains(player.getUniqueId())) {
+ cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata());
+ }
}
}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java
index 0c82c2ca..98608354 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java
@@ -175,13 +175,30 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) {
}
@Override
- public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) {
+ public void startNpcEmoteInternal(UUID npcUuid, int emoteId, int metadata) {
+ List viewers = this.getApolloViewers(npcUuid);
+ if (viewers.isEmpty()) {
+ return;
+ }
+
Emote emote = Emote.builder()
.id(emoteId)
.metadata(metadata)
.build();
- this.cosmeticModule.startNpcEmote(Recipients.ofEveryone(), npcUuid, emote);
+ this.cosmeticModule.startNpcEmote(Recipients.of(viewers), npcUuid, emote);
+ }
+
+ @Override
+ public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) {
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> {
+ Emote emote = Emote.builder()
+ .id(emoteId)
+ .metadata(metadata)
+ .build();
+
+ this.cosmeticModule.startNpcEmote(apolloPlayer, npcUuid, emote);
+ });
}
@Override
@@ -189,6 +206,31 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) {
this.cosmeticModule.stopNpcEmote(Recipients.ofEveryone(), npcUuid);
}
+ @Override
+ public void stopNpcEmoteInternal(UUID npcUuid) {
+ List viewers = this.getApolloViewers(npcUuid);
+ if (viewers.isEmpty()) {
+ return;
+ }
+
+ this.cosmeticModule.stopNpcEmote(Recipients.of(viewers), npcUuid);
+ }
+
+ @Override
+ public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) {
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer ->
+ this.cosmeticModule.stopNpcEmote(apolloPlayer, npcUuid));
+ }
+
+ private List getApolloViewers(UUID npcUuid) {
+ List viewers = new ArrayList<>();
+ for (Player player : this.getNpcViewers(npcUuid)) {
+ Apollo.getPlayerManager().getPlayer(player.getUniqueId()).ifPresent(viewers::add);
+ }
+
+ return viewers;
+ }
+
@Override
public void resetNpcEmotesExample() {
this.cosmeticModule.resetNpcEmotes(Recipients.ofEveryone());
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java
new file mode 100644
index 00000000..39955281
--- /dev/null
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java
@@ -0,0 +1,66 @@
+/*
+ * 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.example.api.module;
+
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.example.module.impl.HeightLimitExample;
+import com.lunarclient.apollo.module.heightlimit.HeightLimit;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import java.util.Optional;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Player;
+
+public class HeightLimitApiExample extends HeightLimitExample {
+
+ private final HeightLimitModule heightLimitModule = Apollo.getModuleManager().getModule(HeightLimitModule.class);
+
+ @Override
+ public void overrideHeightLimitExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+
+ apolloPlayerOpt.ifPresent(apolloPlayer -> {
+ this.heightLimitModule.overrideHeightLimit(apolloPlayer, HeightLimit.builder()
+ .world("world_the_end")
+ .limit(150)
+ .displayName(Component.text("The End", NamedTextColor.DARK_PURPLE))
+ .build()
+ );
+ });
+ }
+
+ @Override
+ public void removeHeightLimitExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.heightLimitModule.removeHeightLimit(apolloPlayer, "world_the_end"));
+ }
+
+ @Override
+ public void resetHeightLimitsExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(this.heightLimitModule::resetHeightLimits);
+ }
+
+}
diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml
index 9ba142c0..c16d024b 100644
--- a/example/bukkit/api/src/main/resources/plugin.yml
+++ b/example/bukkit/api/src/main/resources/plugin.yml
@@ -36,6 +36,8 @@ commands:
description: "Glint!"
glow:
description: "Glow!"
+ heightlimit:
+ description: "Height Limit!"
hologram:
description: "Holograms!"
inventory:
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java
index c39d80f2..433af374 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java
@@ -34,6 +34,7 @@
import com.lunarclient.apollo.example.command.EntityCommand;
import com.lunarclient.apollo.example.command.GlintCommand;
import com.lunarclient.apollo.example.command.GlowCommand;
+import com.lunarclient.apollo.example.command.HeightLimitCommand;
import com.lunarclient.apollo.example.command.HologramCommand;
import com.lunarclient.apollo.example.command.InventoryCommand;
import com.lunarclient.apollo.example.command.LimbCommand;
@@ -68,6 +69,7 @@
import com.lunarclient.apollo.example.module.impl.EntityExample;
import com.lunarclient.apollo.example.module.impl.GlintExample;
import com.lunarclient.apollo.example.module.impl.GlowExample;
+import com.lunarclient.apollo.example.module.impl.HeightLimitExample;
import com.lunarclient.apollo.example.module.impl.HologramExample;
import com.lunarclient.apollo.example.module.impl.InventoryExample;
import com.lunarclient.apollo.example.module.impl.LimbExample;
@@ -91,6 +93,7 @@
import com.lunarclient.apollo.example.module.impl.VignetteExample;
import com.lunarclient.apollo.example.module.impl.WaypointExample;
import com.lunarclient.apollo.example.nms.NpcManager;
+import com.lunarclient.apollo.example.nms.PlayerNpc;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.plugin.java.JavaPlugin;
@@ -114,6 +117,7 @@ public abstract class ApolloExamplePlugin extends JavaPlugin {
private EntityExample entityExample;
private GlintExample glintExample;
private GlowExample glowExample;
+ private HeightLimitExample heightLimitExample;
private HologramExample hologramExample;
private InventoryExample inventoryExample;
private LimbExample limbExample;
@@ -150,6 +154,13 @@ public void onEnable() {
this.registerCommands();
this.registerModuleExamples();
this.registerListeners();
+
+ this.npcManager.addViewerListener((viewer, npc) -> {
+ PlayerNpc.ActiveEmote emote = npc.getActiveEmote();
+ if (emote != null && this.cosmeticExample != null) {
+ this.cosmeticExample.startNpcEmoteToViewer(viewer, npc.getUuid(), emote.getEmoteId(), emote.getMetadata());
+ }
+ });
}
@Override
@@ -173,6 +184,7 @@ private void registerCommonCommands() {
this.getCommand("entity").setExecutor(new EntityCommand());
this.getCommand("glint").setExecutor(new GlintCommand());
this.getCommand("glow").setExecutor(new GlowCommand());
+ this.getCommand("heightlimit").setExecutor(new HeightLimitCommand());
this.getCommand("hologram").setExecutor(new HologramCommand());
this.getCommand("inventory").setExecutor(new InventoryCommand());
this.getCommand("limb").setExecutor(new LimbCommand());
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java
index ad8057e8..92817e47 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java
@@ -222,7 +222,12 @@ private boolean handleEmote(Player player, CosmeticExample example, String[] arg
}
}
- example.startNpcEmoteInternal(player, uuid, emoteId, metadata);
+ example.startNpcEmoteInternal(uuid, emoteId, metadata);
+
+ int emoteMetadata = metadata;
+ ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(uuid)
+ .ifPresent(npc -> npc.setActiveEmote(new PlayerNpc.ActiveEmote(emoteId, emoteMetadata)));
+
player.sendMessage(ChatColor.GREEN + "Started emote " + emoteId + " on NPC " + args[2]);
break;
}
@@ -238,13 +243,22 @@ private boolean handleEmote(Player player, CosmeticExample example, String[] arg
return true;
}
- example.stopNpcEmoteExample(player, uuid);
+ example.stopNpcEmoteInternal(uuid);
+
+ ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(uuid)
+ .ifPresent(npc -> npc.setActiveEmote(null));
+
player.sendMessage(ChatColor.GREEN + "Stopped emote on NPC " + args[2]);
break;
}
case "reset": {
example.resetNpcEmotesExample();
+
+ for (PlayerNpc npc : ApolloExamplePlugin.getInstance().getNpcManager().getNpcs()) {
+ npc.setActiveEmote(null);
+ }
+
player.sendMessage(ChatColor.GREEN + "Reset all NPC emotes");
break;
}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java
new file mode 100644
index 00000000..7a4a3e68
--- /dev/null
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java
@@ -0,0 +1,79 @@
+/*
+ * 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.example.command;
+
+import com.lunarclient.apollo.example.ApolloExamplePlugin;
+import com.lunarclient.apollo.example.module.impl.HeightLimitExample;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class HeightLimitCommand implements CommandExecutor {
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (!(sender instanceof Player)) {
+ sender.sendMessage("Player only!");
+ return true;
+ }
+
+ Player player = (Player) sender;
+
+ if (args.length != 1) {
+ player.sendMessage("Usage: /heightlimit ");
+ return true;
+ }
+
+ HeightLimitExample heightLimitExample = ApolloExamplePlugin.getInstance().getHeightLimitExample();
+
+ switch (args[0].toLowerCase()) {
+ case "override": {
+ heightLimitExample.overrideHeightLimitExample(player);
+ player.sendMessage("Overriding height limit....");
+ break;
+ }
+
+ case "remove": {
+ heightLimitExample.removeHeightLimitExample(player);
+ player.sendMessage("Removing height limit....");
+ break;
+ }
+
+ case "reset": {
+ heightLimitExample.resetHeightLimitsExample(player);
+ player.sendMessage("Resetting height limits...");
+ break;
+ }
+
+ default: {
+ player.sendMessage("Usage: /heightlimit ");
+ break;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java
index 0e56b672..b6e33e20 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java
@@ -23,6 +23,7 @@
*/
package com.lunarclient.apollo.example.module.impl;
+import com.lunarclient.apollo.example.ApolloExamplePlugin;
import com.lunarclient.apollo.example.module.ApolloModuleExample;
import com.lunarclient.apollo.example.nms.CommandCosmetic;
import java.util.Collections;
@@ -54,12 +55,30 @@ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmeti
public abstract void startNpcEmoteExample(Player viewer, UUID npcUuid);
- public abstract void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata);
+ public void startNpcEmoteInternal(UUID npcUuid, int emoteId, int metadata) {
+ for (Player viewer : this.getNpcViewers(npcUuid)) {
+ this.startNpcEmoteToViewer(viewer, npcUuid, emoteId, metadata);
+ }
+ }
+
+ public abstract void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata);
public abstract void stopNpcEmoteExample(Player viewer, UUID npcUuid);
+ public void stopNpcEmoteInternal(UUID npcUuid) {
+ for (Player viewer : this.getNpcViewers(npcUuid)) {
+ this.stopNpcEmoteToViewer(viewer, npcUuid);
+ }
+ }
+
+ public abstract void stopNpcEmoteToViewer(Player viewer, UUID npcUuid);
+
public abstract void resetNpcEmotesExample();
+ protected List getNpcViewers(UUID npcUuid) {
+ return ApolloExamplePlugin.getInstance().getNpcManager().getViewers(npcUuid);
+ }
+
public abstract void displaySprayExample(Player viewer, int sprayId);
public abstract void removeSprayExample(int sprayId);
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java
new file mode 100644
index 00000000..9246625f
--- /dev/null
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.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.example.module.impl;
+
+import com.lunarclient.apollo.example.module.ApolloModuleExample;
+import org.bukkit.entity.Player;
+
+public abstract class HeightLimitExample extends ApolloModuleExample {
+
+ public abstract void overrideHeightLimitExample(Player viewer);
+
+ public abstract void removeHeightLimitExample(Player viewer);
+
+ public abstract void resetHeightLimitsExample(Player viewer);
+
+}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java
index 37e62f7c..5d5ed71a 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java
@@ -46,12 +46,12 @@ public boolean inventoryModuleExample(Player player) {
public void inventoryModuleCommandExample(Player player) {
player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:stone\",Count:1b,components:{\"minecraft:custom_name\":\"UNCLICKABLE\",\"minecraft:custom_data\":{lunar:{unclickable:true}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:paper\",Count:1b,components:{\"minecraft:custom_name\":\"COPY TO CLIPBOARD\",\"minecraft:custom_data\":{lunar:{unclickable:true,copyToClipboard:\"lunarclient.com\"}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:torch\",Count:1b,components:{\"minecraft:custom_name\":\"OPEN URL\",\"minecraft:custom_data\":{lunar:{unclickable:true,openUrl:\"https://lunarclient.com\"}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:book\",Count:1b,components:{\"minecraft:custom_name\":\"SUGGEST COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,suggestCommand:\"/apollo\"}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:writable_book\",Count:1b,components:{\"minecraft:custom_name\":\"RUN COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,runCommand:\"/apollo\"}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:sponge\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE ITEM TOOLTIP\",\"minecraft:custom_data\":{lunar:{unclickable:true,hideItemTooltip:true}}}}}");
- player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:dirt\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE SLOT HIGHTLIGHT\",\"minecraft:custom_data\":{lunar:{unclickable:true,hideSlotHighlight:true}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:paper\",Count:1b,components:{\"minecraft:custom_name\":\"COPY TO CLIPBOARD\",\"minecraft:custom_data\":{lunar:{unclickable:true,copy_to_clipboard:\"lunarclient.com\"}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:torch\",Count:1b,components:{\"minecraft:custom_name\":\"OPEN URL\",\"minecraft:custom_data\":{lunar:{unclickable:true,open_url:\"https://lunarclient.com\"}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:book\",Count:1b,components:{\"minecraft:custom_name\":\"SUGGEST COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,suggest_command:\"/apollo\"}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:writable_book\",Count:1b,components:{\"minecraft:custom_name\":\"RUN COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,run_command:\"/apollo\"}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:sponge\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE ITEM TOOLTIP\",\"minecraft:custom_data\":{lunar:{unclickable:true,hide_item_tooltip:true}}}}}");
+ player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:dirt\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE SLOT HIGHTLIGHT\",\"minecraft:custom_data\":{lunar:{unclickable:true,hide_slot_highlight:true}}}}}");
}
public void inventoryModuleNMSExample(Player player) {
@@ -70,7 +70,7 @@ public void inventoryModuleNMSExample(Player player) {
);
copyToClipboardItem = ItemUtil.addTag(copyToClipboardItem, "unclickable", true);
- inventory.setItem(12, ItemUtil.addTag(copyToClipboardItem, "copyToClipboard", "lunarclient.com"));
+ inventory.setItem(12, ItemUtil.addTag(copyToClipboardItem, "copy_to_clipboard", "lunarclient.com"));
ItemStack openUrlItem = ItemUtil.itemWithName(
Material.TORCH,
@@ -78,7 +78,7 @@ public void inventoryModuleNMSExample(Player player) {
);
openUrlItem = ItemUtil.addTag(openUrlItem, "unclickable", true);
- inventory.setItem(14, ItemUtil.addTag(openUrlItem, "openUrl", "https://lunarclient.com"));
+ inventory.setItem(14, ItemUtil.addTag(openUrlItem, "open_url", "https://lunarclient.com"));
ItemStack suggestCommandItem = ItemUtil.itemWithName(
Material.BOOK,
@@ -86,7 +86,7 @@ public void inventoryModuleNMSExample(Player player) {
);
suggestCommandItem = ItemUtil.addTag(suggestCommandItem, "unclickable", true);
- inventory.setItem(16, ItemUtil.addTag(suggestCommandItem, "suggestCommand", "/apollo"));
+ inventory.setItem(16, ItemUtil.addTag(suggestCommandItem, "suggest_command", "/apollo"));
ItemStack runCommandItem = ItemUtil.itemWithName(
Material.ENCHANTED_BOOK,
@@ -94,7 +94,7 @@ public void inventoryModuleNMSExample(Player player) {
);
runCommandItem = ItemUtil.addTag(runCommandItem, "unclickable", true);
- inventory.setItem(29, ItemUtil.addTag(runCommandItem, "runCommand", "/apollo"));
+ inventory.setItem(29, ItemUtil.addTag(runCommandItem, "run_command", "/apollo"));
ItemStack hideTooltipItem = ItemUtil.itemWithName(
Material.SPONGE,
@@ -102,7 +102,7 @@ public void inventoryModuleNMSExample(Player player) {
);
hideTooltipItem = ItemUtil.addTag(hideTooltipItem, "unclickable", true);
- inventory.setItem(31, ItemUtil.addTag(hideTooltipItem, "hideItemTooltip", true));
+ inventory.setItem(31, ItemUtil.addTag(hideTooltipItem, "hide_item_tooltip", true));
ItemStack hideHighlightItem = ItemUtil.itemWithName(
Material.DIRT,
@@ -110,7 +110,7 @@ public void inventoryModuleNMSExample(Player player) {
);
hideHighlightItem = ItemUtil.addTag(hideHighlightItem, "unclickable", true);
- inventory.setItem(33, ItemUtil.addTag(hideHighlightItem, "hideSlotHighlight", true));
+ inventory.setItem(33, ItemUtil.addTag(hideHighlightItem, "hide_slot_highlight", true));
player.openInventory(inventory);
}
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java
index 391c46d7..4ffba9cd 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java
@@ -36,6 +36,7 @@
import com.lunarclient.apollo.example.json.module.CosmeticJsonExample;
import com.lunarclient.apollo.example.json.module.EntityJsonExample;
import com.lunarclient.apollo.example.json.module.GlowJsonExample;
+import com.lunarclient.apollo.example.json.module.HeightLimitJsonExample;
import com.lunarclient.apollo.example.json.module.HologramJsonExample;
import com.lunarclient.apollo.example.json.module.LimbJsonExample;
import com.lunarclient.apollo.example.json.module.MarkerJsonExample;
@@ -81,6 +82,7 @@ public void registerModuleExamples() {
this.setCooldownExample(new CooldownJsonExample());
this.setEntityExample(new EntityJsonExample());
this.setGlowExample(new GlowJsonExample());
+ this.setHeightLimitExample(new HeightLimitJsonExample());
this.setHologramExample(new HologramJsonExample());
this.setLimbExample(new LimbJsonExample());
this.setMarkerExample(new MarkerJsonExample());
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java
index 6c0984d5..217aab0a 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java
@@ -26,6 +26,9 @@
import com.google.gson.JsonObject;
import com.lunarclient.apollo.example.ApolloExamplePlugin;
import com.lunarclient.apollo.example.json.util.JsonPacketUtil;
+import com.lunarclient.apollo.example.module.impl.CosmeticExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
+import com.lunarclient.apollo.example.nms.PlayerNpc;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@@ -81,6 +84,26 @@ private void onRegisterChannel(PlayerRegisterChannelEvent event) {
PLAYERS_RUNNING_APOLLO.add(player.getUniqueId());
player.sendMessage("You are using LunarClient!");
+
+ this.applyNpcCosmetics(player);
+ }
+
+ private void applyNpcCosmetics(Player player) {
+ CosmeticExample cosmeticExample = this.plugin.getCosmeticExample();
+ if (cosmeticExample == null) {
+ return;
+ }
+
+ for (PlayerNpc npc : this.plugin.getNpcManager().getNpcs()) {
+ for (CommandCosmetic spec : npc.getCosmetics()) {
+ cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec);
+ }
+
+ PlayerNpc.ActiveEmote emote = npc.getActiveEmote();
+ if (emote != null && npc.getViewers().contains(player.getUniqueId())) {
+ cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata());
+ }
+ }
}
@EventHandler
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java
index cff28169..b9a43ef9 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java
@@ -29,6 +29,7 @@
import com.lunarclient.apollo.example.json.util.JsonPacketUtil;
import com.lunarclient.apollo.example.json.util.JsonUtil;
import com.lunarclient.apollo.example.module.impl.CosmeticExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
@@ -103,6 +104,56 @@ public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List
JsonPacketUtil.broadcastPacket(message);
}
+ @Override
+ public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ JsonPacketUtil.broadcastPacket(this.createEquipMessage(npcUuid, cosmetic));
+ }
+
+ @Override
+ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ JsonPacketUtil.sendPacket(viewer, this.createEquipMessage(npcUuid, cosmetic));
+ }
+
+ private JsonObject createEquipMessage(UUID npcUuid, CommandCosmetic cosmetic) {
+ JsonObject cosmeticObject = new JsonObject();
+ cosmeticObject.addProperty("id", cosmetic.getId());
+
+ CommandCosmetic.Options options = cosmetic.getOptions();
+ if (options instanceof CommandCosmetic.Hat) {
+ CommandCosmetic.Hat hat = (CommandCosmetic.Hat) options;
+ JsonObject hatOptions = new JsonObject();
+ hatOptions.addProperty("show_over_helmet", hat.isShowOverHelmet());
+ hatOptions.addProperty("show_over_skin_layer", hat.isShowOverSkinLayer());
+ hatOptions.addProperty("height_offset", hat.getHeightOffset());
+ cosmeticObject.add("hat_options", hatOptions);
+ } else if (options instanceof CommandCosmetic.Cloak) {
+ JsonObject cloakOptions = new JsonObject();
+ cloakOptions.addProperty("use_cloth_physics", ((CommandCosmetic.Cloak) options).isUseClothPhysics());
+ cosmeticObject.add("cloak_options", cloakOptions);
+ } else if (options instanceof CommandCosmetic.Pet) {
+ JsonObject petOptions = new JsonObject();
+ petOptions.addProperty("flip_shoulder", ((CommandCosmetic.Pet) options).isFlipShoulder());
+ cosmeticObject.add("pet_options", petOptions);
+ } else if (options instanceof CommandCosmetic.Body) {
+ CommandCosmetic.Body body = (CommandCosmetic.Body) options;
+ JsonObject bodyOptions = new JsonObject();
+ bodyOptions.addProperty("show_over_chestplate", body.isShowOverChestplate());
+ bodyOptions.addProperty("show_over_leggings", body.isShowOverLeggings());
+ bodyOptions.addProperty("show_over_boots", body.isShowOverBoots());
+ cosmeticObject.add("body_options", bodyOptions);
+ }
+
+ JsonArray cosmeticsArray = new JsonArray();
+ cosmeticsArray.add(cosmeticObject);
+
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage");
+ message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid));
+ message.add("cosmetics", cosmeticsArray);
+
+ return message;
+ }
+
@Override
public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) {
List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977);
@@ -154,7 +205,7 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) {
}
@Override
- public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) {
+ public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) {
JsonObject emote = new JsonObject();
emote.addProperty("id", emoteId);
emote.addProperty("metadata", metadata);
@@ -164,7 +215,7 @@ public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int
message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid));
message.add("emote", emote);
- JsonPacketUtil.broadcastPacket(message);
+ JsonPacketUtil.sendPacket(viewer, message);
}
@Override
@@ -176,6 +227,15 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) {
JsonPacketUtil.broadcastPacket(message);
}
+ @Override
+ public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.StopNpcEmoteMessage");
+ message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid));
+
+ JsonPacketUtil.sendPacket(viewer, message);
+ }
+
@Override
public void resetNpcEmotesExample() {
JsonObject message = new JsonObject();
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java
new file mode 100644
index 00000000..37f8e89d
--- /dev/null
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java
@@ -0,0 +1,66 @@
+/*
+ * 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.example.json.module;
+
+import com.google.gson.JsonObject;
+import com.lunarclient.apollo.example.json.util.AdventureUtil;
+import com.lunarclient.apollo.example.json.util.JsonPacketUtil;
+import com.lunarclient.apollo.example.module.impl.HeightLimitExample;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Player;
+
+public class HeightLimitJsonExample extends HeightLimitExample {
+
+ @Override
+ public void overrideHeightLimitExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage");
+ message.addProperty("world", "world_the_end");
+ message.addProperty("limit", 150);
+ message.addProperty("display_name_adventure_json_lines", AdventureUtil.toJson(
+ Component.text("The End", NamedTextColor.DARK_PURPLE)
+ ));
+
+ JsonPacketUtil.sendPacket(viewer, message);
+ }
+
+ @Override
+ public void removeHeightLimitExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage");
+ message.addProperty("world", "world_the_end");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+ }
+
+ @Override
+ public void resetHeightLimitsExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+ }
+
+}
diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml
index 334fe2a4..0b4a5910 100644
--- a/example/bukkit/json/src/main/resources/plugin.yml
+++ b/example/bukkit/json/src/main/resources/plugin.yml
@@ -32,6 +32,8 @@ commands:
description: "Glint!"
glow:
description: "Glow!"
+ heightlimit:
+ description: "Height Limit!"
hologram:
description: "Holograms!"
inventory:
@@ -52,6 +54,8 @@ commands:
description: "Pay Now!"
richpresence:
description: "Rich Presence!"
+ serverlink:
+ description: "Server Links!"
saturation:
description: "Saturation!"
serverrule:
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java
index be00b996..37f2e2e5 100644
--- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java
@@ -26,6 +26,7 @@
import com.mojang.authlib.GameProfile;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -51,10 +52,12 @@
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
-import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Nullable;
@@ -65,7 +68,11 @@ public final class NpcManager implements Listener {
HumanoidArm.RIGHT, false, false, ParticleStatus.ALL
);
+ private static final double TRACKING_RANGE = 48.0;
+ private static final double TRACKING_RANGE_SQUARED = TRACKING_RANGE * TRACKING_RANGE;
+
private final Map npcs = new HashMap<>();
+ private final List viewerListeners = new ArrayList<>();
private final JavaPlugin plugin;
private final NpcStore store;
@@ -75,6 +82,11 @@ public NpcManager(JavaPlugin plugin) {
Bukkit.getPluginManager().registerEvents(this, plugin);
Bukkit.getScheduler().runTask(plugin, this::loadOrSpawnDefaults);
+ Bukkit.getScheduler().runTaskTimer(plugin, this::updateVisibility, 1L, 10L);
+ }
+
+ public void addViewerListener(NpcViewerListener listener) {
+ this.viewerListeners.add(listener);
}
public void removeNpc(UUID uuid) {
@@ -114,21 +126,73 @@ public Collection getNpcs() {
}
@EventHandler
- public void onPlayerJoin(PlayerJoinEvent event) {
- ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle();
- for (PlayerNpc npc : this.npcs.values()) {
- this.showNpc(viewer, npc);
- }
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ this.forgetViewer(event.getPlayer());
}
@EventHandler
- public void onPlayerQuit(PlayerQuitEvent event) {
- ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle();
+ public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
+ this.forgetViewer(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onPlayerRespawn(PlayerRespawnEvent event) {
+ this.forgetViewer(event.getPlayer());
+ }
+
+ private void forgetViewer(Player player) {
for (PlayerNpc npc : this.npcs.values()) {
- this.hideNpc(viewer, npc);
+ npc.getViewers().remove(player.getUniqueId());
+ }
+ }
+
+ private void updateVisibility() {
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ for (PlayerNpc npc : this.npcs.values()) {
+ boolean inRange = this.isWithinTrackingRange(player, npc);
+ boolean viewing = npc.getViewers().contains(player.getUniqueId());
+
+ if (inRange && !viewing) {
+ npc.getViewers().add(player.getUniqueId());
+ this.showNpc(((CraftPlayer) player).getHandle(), npc);
+
+ for (NpcViewerListener listener : this.viewerListeners) {
+ listener.onNpcShown(player, npc);
+ }
+ } else if (!inRange && viewing) {
+ npc.getViewers().remove(player.getUniqueId());
+ this.hideNpc(((CraftPlayer) player).getHandle(), npc);
+ }
+ }
}
}
+ private boolean isWithinTrackingRange(Player player, PlayerNpc npc) {
+ Location location = npc.getLocation();
+ World world = location.getWorld();
+
+ return world != null
+ && world.equals(player.getWorld())
+ && player.getLocation().distanceSquared(location) <= NpcManager.TRACKING_RANGE_SQUARED;
+ }
+
+ public List getViewers(UUID npcUuid) {
+ PlayerNpc npc = this.npcs.get(npcUuid);
+ if (npc == null) {
+ return Collections.emptyList();
+ }
+
+ List viewers = new ArrayList<>();
+ for (UUID viewerUuid : npc.getViewers()) {
+ Player player = Bukkit.getPlayer(viewerUuid);
+ if (player != null) {
+ viewers.add(player);
+ }
+ }
+
+ return viewers;
+ }
+
private void loadOrSpawnDefaults() {
if (!this.store.exists()) {
this.spawnDefaultNpcs();
@@ -184,9 +248,7 @@ private void spawnDefaultNpcs() {
PlayerNpc playerNpc = new PlayerNpc(npc.getUUID(), name, location.clone(), npc);
this.npcs.put(playerNpc.getUuid(), playerNpc);
- for (ServerPlayer viewer : server.getPlayerList().getPlayers()) {
- this.showNpc(viewer, playerNpc);
- }
+ this.updateVisibility();
return playerNpc;
}
@@ -220,9 +282,14 @@ private void hideNpc(ServerPlayer viewer, PlayerNpc npc) {
}
private void despawnNpcs(PlayerNpc npc) {
- for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) {
- this.hideNpc(player, npc);
+ for (UUID viewerUuid : npc.getViewers()) {
+ Player player = Bukkit.getPlayer(viewerUuid);
+ if (player != null) {
+ this.hideNpc(((CraftPlayer) player).getHandle(), npc);
+ }
}
+
+ npc.getViewers().clear();
}
}
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java
new file mode 100644
index 00000000..479e3eeb
--- /dev/null
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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.example.nms;
+
+import org.bukkit.entity.Player;
+
+@FunctionalInterface
+public interface NpcViewerListener {
+
+ void onNpcShown(Player viewer, PlayerNpc npc);
+
+}
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java
index 9bdde6f9..fa464bc3 100644
--- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java
@@ -24,13 +24,16 @@
package com.lunarclient.apollo.example.nms;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.Location;
+import org.jetbrains.annotations.Nullable;
@Getter
@RequiredArgsConstructor
@@ -40,12 +43,26 @@ public final class PlayerNpc {
private final String name;
private final Location location;
private final ServerPlayer handle;
+ private final Set viewers = new HashSet<>();
@Setter
private List cosmetics = new ArrayList<>();
+ @Setter
+ @Nullable
+ private ActiveEmote activeEmote;
+
public int getEntityId() {
return this.handle.getId();
}
+ @Getter
+ @RequiredArgsConstructor
+ public static final class ActiveEmote {
+
+ private final int emoteId;
+ private final int metadata;
+
+ }
+
}
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java
index 94578ad4..5c0b2a5c 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java
@@ -36,6 +36,7 @@
import com.lunarclient.apollo.example.proto.module.CosmeticProtoExample;
import com.lunarclient.apollo.example.proto.module.EntityProtoExample;
import com.lunarclient.apollo.example.proto.module.GlowProtoExample;
+import com.lunarclient.apollo.example.proto.module.HeightLimitProtoExample;
import com.lunarclient.apollo.example.proto.module.HologramProtoExample;
import com.lunarclient.apollo.example.proto.module.LimbProtoExample;
import com.lunarclient.apollo.example.proto.module.MarkerProtoExample;
@@ -81,6 +82,7 @@ public void registerModuleExamples() {
this.setCooldownExample(new CooldownProtoExample());
this.setEntityExample(new EntityProtoExample());
this.setGlowExample(new GlowProtoExample());
+ this.setHeightLimitExample(new HeightLimitProtoExample());
this.setHologramExample(new HologramProtoExample());
this.setLimbExample(new LimbProtoExample());
this.setMarkerExample(new MarkerProtoExample());
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java
index a9aee632..6e55ce2a 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java
@@ -24,6 +24,9 @@
package com.lunarclient.apollo.example.proto.listener;
import com.lunarclient.apollo.example.ApolloExamplePlugin;
+import com.lunarclient.apollo.example.module.impl.CosmeticExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
+import com.lunarclient.apollo.example.nms.PlayerNpc;
import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil;
import com.lunarclient.apollo.player.v1.UpdatePlayerWorldMessage;
import java.util.HashSet;
@@ -79,6 +82,26 @@ private void onRegisterChannel(PlayerRegisterChannelEvent event) {
PLAYERS_RUNNING_APOLLO.add(player.getUniqueId());
player.sendMessage("You are using LunarClient!");
+
+ this.applyNpcCosmetics(player);
+ }
+
+ private void applyNpcCosmetics(Player player) {
+ CosmeticExample cosmeticExample = this.plugin.getCosmeticExample();
+ if (cosmeticExample == null) {
+ return;
+ }
+
+ for (PlayerNpc npc : this.plugin.getNpcManager().getNpcs()) {
+ for (CommandCosmetic spec : npc.getCosmetics()) {
+ cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec);
+ }
+
+ PlayerNpc.ActiveEmote emote = npc.getActiveEmote();
+ if (emote != null && npc.getViewers().contains(player.getUniqueId())) {
+ cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata());
+ }
+ }
}
@EventHandler
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java
index e83851d7..f66f03aa 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java
@@ -24,11 +24,13 @@
package com.lunarclient.apollo.example.proto.module;
import com.google.common.collect.Lists;
+import com.lunarclient.apollo.cosmetic.v1.BodyOptions;
import com.lunarclient.apollo.cosmetic.v1.CloakOptions;
import com.lunarclient.apollo.cosmetic.v1.Cosmetic;
import com.lunarclient.apollo.cosmetic.v1.DisplaySprayMessage;
import com.lunarclient.apollo.cosmetic.v1.Emote;
import com.lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage;
+import com.lunarclient.apollo.cosmetic.v1.HatOptions;
import com.lunarclient.apollo.cosmetic.v1.PetOptions;
import com.lunarclient.apollo.cosmetic.v1.RemoveSprayMessage;
import com.lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage;
@@ -38,6 +40,7 @@
import com.lunarclient.apollo.cosmetic.v1.StopNpcEmoteMessage;
import com.lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage;
import com.lunarclient.apollo.example.module.impl.CosmeticExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil;
import com.lunarclient.apollo.example.proto.util.ProtobufUtil;
import com.lunarclient.apollo.packetenrichment.v1.Direction;
@@ -109,6 +112,51 @@ public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List
ProtobufPacketUtil.broadcastPacket(message);
}
+ @Override
+ public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ ProtobufPacketUtil.broadcastPacket(this.createEquipMessage(npcUuid, cosmetic));
+ }
+
+ @Override
+ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ ProtobufPacketUtil.sendPacket(viewer, this.createEquipMessage(npcUuid, cosmetic));
+ }
+
+ private EquipNpcCosmeticsMessage createEquipMessage(UUID npcUuid, CommandCosmetic cosmetic) {
+ Cosmetic.Builder cosmeticBuilder = Cosmetic.newBuilder()
+ .setId(cosmetic.getId());
+
+ CommandCosmetic.Options options = cosmetic.getOptions();
+ if (options instanceof CommandCosmetic.Hat) {
+ CommandCosmetic.Hat hat = (CommandCosmetic.Hat) options;
+ cosmeticBuilder.setHatOptions(HatOptions.newBuilder()
+ .setShowOverHelmet(hat.isShowOverHelmet())
+ .setShowOverSkinLayer(hat.isShowOverSkinLayer())
+ .setHeightOffset(hat.getHeightOffset())
+ .build());
+ } else if (options instanceof CommandCosmetic.Cloak) {
+ cosmeticBuilder.setCloakOptions(CloakOptions.newBuilder()
+ .setUseClothPhysics(((CommandCosmetic.Cloak) options).isUseClothPhysics())
+ .build());
+ } else if (options instanceof CommandCosmetic.Pet) {
+ cosmeticBuilder.setPetOptions(PetOptions.newBuilder()
+ .setFlipShoulder(((CommandCosmetic.Pet) options).isFlipShoulder())
+ .build());
+ } else if (options instanceof CommandCosmetic.Body) {
+ CommandCosmetic.Body body = (CommandCosmetic.Body) options;
+ cosmeticBuilder.setBodyOptions(BodyOptions.newBuilder()
+ .setShowOverChestplate(body.isShowOverChestplate())
+ .setShowOverLeggings(body.isShowOverLeggings())
+ .setShowOverBoots(body.isShowOverBoots())
+ .build());
+ }
+
+ return EquipNpcCosmeticsMessage.newBuilder()
+ .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid))
+ .addCosmetics(cosmeticBuilder.build())
+ .build();
+ }
+
@Override
public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) {
List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977);
@@ -153,7 +201,7 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) {
}
@Override
- public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) {
+ public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) {
StartNpcEmoteMessage message = StartNpcEmoteMessage.newBuilder()
.setNpcUuid(ProtobufUtil.createUuidProto(npcUuid))
.setEmote(Emote.newBuilder()
@@ -162,7 +210,7 @@ public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int
.build())
.build();
- ProtobufPacketUtil.broadcastPacket(message);
+ ProtobufPacketUtil.sendPacket(viewer, message);
}
@Override
@@ -174,6 +222,15 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) {
ProtobufPacketUtil.broadcastPacket(message);
}
+ @Override
+ public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) {
+ StopNpcEmoteMessage message = StopNpcEmoteMessage.newBuilder()
+ .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid))
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+ }
+
@Override
public void resetNpcEmotesExample() {
ResetNpcEmotesMessage message = ResetNpcEmotesMessage.getDefaultInstance();
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java
new file mode 100644
index 00000000..babbce5b
--- /dev/null
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java
@@ -0,0 +1,66 @@
+/*
+ * 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.example.proto.module;
+
+import com.lunarclient.apollo.example.module.impl.HeightLimitExample;
+import com.lunarclient.apollo.example.proto.util.AdventureUtil;
+import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil;
+import com.lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage;
+import com.lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage;
+import com.lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Player;
+
+public class HeightLimitProtoExample extends HeightLimitExample {
+
+ @Override
+ public void overrideHeightLimitExample(Player viewer) {
+ OverrideHeightLimitMessage message = OverrideHeightLimitMessage.newBuilder()
+ .setWorld("world_the_end")
+ .setLimit(150)
+ .setDisplayNameAdventureJsonLines(AdventureUtil.toJson(
+ Component.text("The End", NamedTextColor.DARK_PURPLE)
+ ))
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+ }
+
+ @Override
+ public void removeHeightLimitExample(Player viewer) {
+ RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder()
+ .setWorld("world_the_end")
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+ }
+
+ @Override
+ public void resetHeightLimitsExample(Player viewer) {
+ ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance();
+ ProtobufPacketUtil.sendPacket(viewer, message);
+ }
+
+}
diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml
index f74f917e..bab850d2 100644
--- a/example/bukkit/proto/src/main/resources/plugin.yml
+++ b/example/bukkit/proto/src/main/resources/plugin.yml
@@ -32,6 +32,8 @@ commands:
description: "Glint!"
glow:
description: "Glow!"
+ heightlimit:
+ description: "Height Limit!"
hologram:
description: "Holograms!"
inventory:
@@ -52,6 +54,8 @@ commands:
description: "Pay Now!"
richpresence:
description: "Rich Presence!"
+ serverlink:
+ description: "Server Links!"
saturation:
description: "Saturation!"
serverrule:
diff --git a/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java b/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java
index 93c11e7a..04176482 100644
--- a/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java
+++ b/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java
@@ -68,6 +68,30 @@ public static String toLegacy(@NonNull Component component) {
return LegacyComponentSerializer.legacySection().serialize(component);
}
+ /**
+ * Returns a new component from the provided legacy {@link String},
+ * using {@code &} color codes.
+ *
+ * @param legacy the legacy string for this component
+ * @return the component from the legacy string
+ * @since 1.2.9
+ */
+ public static Component fromLegacyAmpersand(@NonNull String legacy) {
+ return LegacyComponentSerializer.legacyAmpersand().deserialize(legacy);
+ }
+
+ /**
+ * Returns this component as a legacy {@link String},
+ * using {@code &} color codes.
+ *
+ * @param component the component to make into a legacy string
+ * @return the legacy string for this component
+ * @since 1.2.9
+ */
+ public static String toLegacyAmpersand(@NonNull Component component) {
+ return LegacyComponentSerializer.legacyAmpersand().serialize(component);
+ }
+
private ApolloComponent() {
}
diff --git a/gradle.properties b/gradle.properties
index 1cecc874..43cc0bbd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
group=com.lunarclient
-version=1.2.8
+version=1.2.9-SNAPSHOT
description=The API for interacting with Lunar Client players.
org.gradle.parallel=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d83b4d64..8f52700a 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.0"
+protobuf = "0.2.1"
gson = "2.10.1"
shadow = "9.4.1"
spotless = "8.4.0"
diff --git a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java
index 6de2e988..c485822d 100644
--- a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java
+++ b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java
@@ -50,6 +50,8 @@
import com.lunarclient.apollo.module.glint.GlintModule;
import com.lunarclient.apollo.module.glow.GlowModule;
import com.lunarclient.apollo.module.glow.GlowModuleImpl;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl;
import com.lunarclient.apollo.module.hologram.HologramModule;
import com.lunarclient.apollo.module.hologram.HologramModuleImpl;
import com.lunarclient.apollo.module.inventory.InventoryModule;
@@ -149,6 +151,7 @@ public void onEnable() {
.addModule(EntityModule.class, new EntityModuleImpl())
.addModule(GlintModule.class)
.addModule(GlowModule.class, new GlowModuleImpl())
+ .addModule(HeightLimitModule.class, new HeightLimitModuleImpl())
.addModule(HologramModule.class, new HologramModuleImpl())
.addModule(InventoryModule.class)
.addModule(LimbModule.class, new LimbModuleImpl())
diff --git a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java
index a5f50d27..74215459 100644
--- a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java
+++ b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java
@@ -46,6 +46,8 @@
import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl;
import com.lunarclient.apollo.module.entity.EntityModule;
import com.lunarclient.apollo.module.entity.EntityModuleImpl;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl;
import com.lunarclient.apollo.module.hologram.HologramModule;
import com.lunarclient.apollo.module.hologram.HologramModuleImpl;
import com.lunarclient.apollo.module.limb.LimbModule;
@@ -131,6 +133,7 @@ public void onEnable() {
.addModule(CombatModule.class)
.addModule(CooldownModule.class, new CooldownModuleImpl())
.addModule(EntityModule.class, new EntityModuleImpl())
+ .addModule(HeightLimitModule.class, new HeightLimitModuleImpl())
.addModule(HologramModule.class, new HologramModuleImpl())
.addModule(LimbModule.class, new LimbModuleImpl())
.addModule(MarkerModule.class, new MarkerModuleImpl())
diff --git a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java
index 19a4878b..5255e8e3 100644
--- a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java
+++ b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java
@@ -48,6 +48,8 @@
import com.lunarclient.apollo.module.entity.EntityModuleImpl;
import com.lunarclient.apollo.module.glow.GlowModule;
import com.lunarclient.apollo.module.glow.GlowModuleImpl;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl;
import com.lunarclient.apollo.module.hologram.HologramModule;
import com.lunarclient.apollo.module.hologram.HologramModuleImpl;
import com.lunarclient.apollo.module.limb.LimbModule;
@@ -138,6 +140,7 @@ public void onEnable() {
.addModule(CooldownModule.class, new CooldownModuleImpl())
.addModule(EntityModule.class, new EntityModuleImpl())
.addModule(GlowModule.class, new GlowModuleImpl())
+ .addModule(HeightLimitModule.class, new HeightLimitModuleImpl())
.addModule(HologramModule.class, new HologramModuleImpl())
.addModule(LimbModule.class, new LimbModuleImpl())
.addModule(MarkerModule.class, new MarkerModuleImpl())
diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java
index 8c2e26ab..b1d97ad7 100644
--- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java
+++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java
@@ -49,6 +49,8 @@
import com.lunarclient.apollo.module.glint.GlintModule;
import com.lunarclient.apollo.module.glow.GlowModule;
import com.lunarclient.apollo.module.glow.GlowModuleImpl;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl;
import com.lunarclient.apollo.module.hologram.HologramModule;
import com.lunarclient.apollo.module.hologram.HologramModuleImpl;
import com.lunarclient.apollo.module.inventory.InventoryModule;
@@ -171,6 +173,7 @@ public static void init(ApolloMinestomProperties properties) {
.addModule(EntityModule.class, new EntityModuleImpl())
.addModule(GlintModule.class)
.addModule(GlowModule.class, new GlowModuleImpl())
+ .addModule(HeightLimitModule.class, new HeightLimitModuleImpl())
.addModule(HologramModule.class, new HologramModuleImpl())
.addModule(InventoryModule.class)
.addModule(LimbModule.class, new LimbModuleImpl())
diff --git a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java
index 8d3ed35f..3b801511 100644
--- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java
+++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java
@@ -46,6 +46,8 @@
import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl;
import com.lunarclient.apollo.module.entity.EntityModule;
import com.lunarclient.apollo.module.entity.EntityModuleImpl;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModule;
+import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl;
import com.lunarclient.apollo.module.hologram.HologramModule;
import com.lunarclient.apollo.module.hologram.HologramModuleImpl;
import com.lunarclient.apollo.module.limb.LimbModule;
@@ -198,6 +200,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) {
.addModule(CombatModule.class)
.addModule(CooldownModule.class, new CooldownModuleImpl())
.addModule(EntityModule.class, new EntityModuleImpl())
+ .addModule(HeightLimitModule.class, new HeightLimitModuleImpl())
.addModule(HologramModule.class, new HologramModuleImpl())
.addModule(LimbModule.class, new LimbModuleImpl())
.addModule(MarkerModule.class, new MarkerModuleImpl())