diff --git a/api/src/main/java/com/lunarclient/apollo/common/icon/CustomModelData.java b/api/src/main/java/com/lunarclient/apollo/common/icon/CustomModelData.java
new file mode 100644
index 00000000..87293405
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/common/icon/CustomModelData.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.common.icon;
+
+import java.util.Collections;
+import java.util.List;
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * Represents the custom model data attached to an {@link ItemStackIcon}.
+ *
+ *
Mirrors the Minecraft 1.21.4 {@code CustomModelData} item component, which
+ * replaced the single integer value with four parallel lists.
+ *
+ * @since 1.2.7
+ */
+@Getter
+@Builder
+public final class CustomModelData {
+
+ /**
+ * Returns the custom model data {@link Float} values.
+ *
+ * @return the custom model data floats
+ * @since 1.2.7
+ */
+ @Builder.Default
+ List floats = Collections.emptyList();
+
+ /**
+ * Returns the custom model data {@link Boolean} flags.
+ *
+ * @return the custom model data flags
+ * @since 1.2.7
+ */
+ @Builder.Default
+ List flags = Collections.emptyList();
+
+ /**
+ * Returns the custom model data {@link String} values.
+ *
+ * @return the custom model data strings
+ * @since 1.2.7
+ */
+ @Builder.Default
+ List strings = Collections.emptyList();
+
+ /**
+ * Returns the custom model data color {@link Integer} values.
+ *
+ * @return the custom model data colors
+ * @since 1.2.7
+ */
+ @Builder.Default
+ List colors = Collections.emptyList();
+
+}
diff --git a/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java b/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java
index 2df8cb7f..3913eee7 100644
--- a/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java
+++ b/api/src/main/java/com/lunarclient/apollo/common/icon/ItemStackIcon.java
@@ -57,10 +57,20 @@ public final class ItemStackIcon extends Icon {
* Returns the icon {@link Integer} custom model data.
*
* @return the icon custom model data
+ * @deprecated for removal since 1.2.7, use {@link ItemStackIcon#customModelDataObject} instead.
* @since 1.0.7
*/
+ @Deprecated
int customModelData;
+ /**
+ * Returns the icon {@link CustomModelData} object.
+ *
+ * @return the icon custom model data object
+ * @since 1.2.7
+ */
+ @Nullable CustomModelData customModelDataObject;
+
/**
* Returns the icon {@link Profile}.
*
diff --git a/api/src/main/java/com/lunarclient/apollo/module/coloredfire/ColoredFireModule.java b/api/src/main/java/com/lunarclient/apollo/module/coloredfire/ColoredFireModule.java
index f63dcaba..de217a19 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/coloredfire/ColoredFireModule.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/coloredfire/ColoredFireModule.java
@@ -25,7 +25,10 @@
import com.lunarclient.apollo.module.ApolloModule;
import com.lunarclient.apollo.module.ModuleDefinition;
+import com.lunarclient.apollo.option.Option;
+import com.lunarclient.apollo.option.SimpleOption;
import com.lunarclient.apollo.recipients.Recipients;
+import io.leangen.geantyref.TypeToken;
import java.awt.Color;
import java.util.UUID;
import org.jetbrains.annotations.ApiStatus;
@@ -39,6 +42,22 @@
@ModuleDefinition(id = "colored_fire", name = "Colored Fire")
public abstract class ColoredFireModule extends ApolloModule {
+ /**
+ * Whether fire colors should persist when a player unloads from the tracker.
+ *
+ * @since 1.2.7
+ */
+ public static final SimpleOption PERSIST_COLORS_ON_UNLOAD = Option.builder()
+ .comment("Set to 'true' to keep fire colors when players unload from the tracker, otherwise 'false'.")
+ .node("persist-colors-on-unload").type(TypeToken.get(Boolean.class))
+ .defaultValue(false).notifyClient().build();
+
+ ColoredFireModule() {
+ this.registerOptions(
+ ColoredFireModule.PERSIST_COLORS_ON_UNLOAD
+ );
+ }
+
@Override
public boolean isClientNotify() {
return true;
diff --git a/api/src/main/java/com/lunarclient/apollo/module/waypoint/Waypoint.java b/api/src/main/java/com/lunarclient/apollo/module/waypoint/Waypoint.java
index 39c534b3..57a1d4db 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/waypoint/Waypoint.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/waypoint/Waypoint.java
@@ -27,6 +27,7 @@
import java.awt.Color;
import lombok.Builder;
import lombok.Getter;
+import org.jetbrains.annotations.Nullable;
/**
* Represents a waypoint which can be shown on the client.
@@ -82,4 +83,45 @@ public final class Waypoint {
*/
boolean hidden;
+ /**
+ * Whether the beacon beam for this waypoint is drawn.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean showBeam = true;
+
+ /**
+ * Whether the in-world block outline highlight at the waypoint location is drawn.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean highlightBlock = true;
+
+ /**
+ * Line width of the block highlight outline when {@link #highlightBlock} is enabled.
+ *
+ * Accepts {@code 1.5F} to {@code 7.5F}. Leaving the
+ * field at its default of {@code 0.0F} is treated as a
+ * fallback to the player's own configured line width.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ float highlightBlockLineWidth = 0.0F;
+
+ /**
+ * Label and HUD text overrides applied to this waypoint.
+ *
+ * Set to a built {@link WaypointTextStyle} to drive the
+ * client's text rendering from the server; leave {@code null} to
+ * defer to the Lunar waypoint mod's own settings.
+ *
+ * @return the text style, or {@code null} when no override is sent
+ * @since 1.2.7
+ */
+ @Builder.Default
+ @Nullable WaypointTextStyle textStyle = null;
+
}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModule.java b/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModule.java
index 176adab5..9b278d1b 100644
--- a/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModule.java
+++ b/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModule.java
@@ -127,4 +127,22 @@ public boolean isClientNotify() {
*/
public abstract void resetWaypoints(Recipients recipients);
+ /**
+ * Shows a {@link Waypoint} for the {@link Recipients} that was previously hidden.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param waypointName the waypoint name
+ * @since 1.2.7
+ */
+ public abstract void showWaypoint(Recipients recipients, String waypointName);
+
+ /**
+ * Hides a {@link Waypoint} for the {@link Recipients} without removing it.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param waypointName the waypoint name
+ * @since 1.2.7
+ */
+ public abstract void hideWaypoint(Recipients recipients, String waypointName);
+
}
diff --git a/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointTextStyle.java b/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointTextStyle.java
new file mode 100644
index 00000000..f34be6c8
--- /dev/null
+++ b/api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointTextStyle.java
@@ -0,0 +1,116 @@
+/*
+ * 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.waypoint;
+
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * Overrides for a waypoint's on-screen label, icons and distance HUD on the Lunar client.
+ *
+ * @since 1.2.7
+ */
+@Getter
+@Builder
+public final class WaypointTextStyle {
+
+ /**
+ * Whether the waypoint label (text and box) is drawn at all.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean showText = true;
+
+ /**
+ * Restricts the waypoint label drawing to when the player is looking roughly toward the waypoint.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean onlyShowTextWhenLookingNear = false;
+
+ /**
+ * Whether the waypoint icons are drawn.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean showIcons = false;
+
+ /**
+ * The waypoint scale applied to the label icon.
+ *
+ * Accepts {@code 0.1F} to {@code 3.0F}
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ float textIconScale = 1.5F;
+
+ /**
+ * The waypoint scale applied to the main label text.
+ *
+ * Accepts {@code 0.1F} to {@code 2.0F}
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ float labelScale = 1.0F;
+
+ /**
+ * The waypoint padding of the label background box.
+ *
+ * Accepts {@code 1.0F} to {@code 8.0F}
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ float boxPadding = 4.0F;
+
+ /**
+ * Whether a border is drawn around the label background box.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean boxBorders = true;
+
+ /**
+ * Whether a shadow is drawn behind the label text.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean textShadow = false;
+
+ /**
+ * Whether the distance from the player to the waypoint is shown next to the label.
+ *
+ * @since 1.2.7
+ */
+ @Builder.Default
+ boolean showDistance = true;
+
+}
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 608563cc..03844a1f 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
@@ -31,8 +31,10 @@
import com.lunarclient.apollo.player.ApolloPlayer;
import com.lunarclient.apollo.recipients.Recipients;
import com.lunarclient.apollo.waypoint.v1.DisplayWaypointMessage;
+import com.lunarclient.apollo.waypoint.v1.HideWaypointMessage;
import com.lunarclient.apollo.waypoint.v1.RemoveWaypointMessage;
import com.lunarclient.apollo.waypoint.v1.ResetWaypointsMessage;
+import com.lunarclient.apollo.waypoint.v1.ShowWaypointMessage;
import java.awt.Color;
import java.lang.reflect.Type;
import java.util.Arrays;
@@ -43,6 +45,8 @@
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
+import static com.lunarclient.apollo.util.Ranges.checkRange;
+
/**
* Provides the waypoints module.
*
@@ -87,6 +91,24 @@ public void resetWaypoints(@NonNull Recipients recipients) {
recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message));
}
+ @Override
+ public void showWaypoint(@NonNull Recipients recipients, @NonNull String waypointName) {
+ ShowWaypointMessage message = ShowWaypointMessage.newBuilder()
+ .setName(waypointName)
+ .build();
+
+ recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message));
+ }
+
+ @Override
+ public void hideWaypoint(@NonNull Recipients recipients, @NonNull String waypointName) {
+ HideWaypointMessage message = HideWaypointMessage.newBuilder()
+ .setName(waypointName)
+ .build();
+
+ recipients.forEach(player -> ((AbstractApolloPlayer) player).sendPacket(message));
+ }
+
private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
ApolloPlayer player = event.getPlayer();
List waypoints = this.getOptions().get(player, WaypointModule.DEFAULT_WAYPOINTS);
@@ -99,31 +121,74 @@ private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
}
private DisplayWaypointMessage toProtobuf(Waypoint waypoint) {
- return DisplayWaypointMessage.newBuilder()
+ DisplayWaypointMessage.Builder builder = DisplayWaypointMessage.newBuilder()
.setName(waypoint.getName())
.setLocation(NetworkTypes.toProtobuf(waypoint.getLocation()))
.setColor(NetworkTypes.toProtobuf(waypoint.getColor()))
.setPreventRemoval(waypoint.isPreventRemoval())
.setHidden(waypoint.isHidden())
+ .setShowBeam(waypoint.isShowBeam())
+ .setHighlightBlock(waypoint.isHighlightBlock());
+
+ float highlightBlockLineWidth = waypoint.getHighlightBlockLineWidth();
+ if (highlightBlockLineWidth != 0.0F) {
+ builder.setHighlightBlockLineWidth(checkRange(highlightBlockLineWidth, 1.5F, 7.5F, "Waypoint#highlightBlockLineWidth"));
+ }
+
+ WaypointTextStyle style = waypoint.getTextStyle();
+ if (style != null) {
+ builder.setStyle(this.toProtobuf(style));
+ }
+
+ return builder.build();
+ }
+
+ private com.lunarclient.apollo.waypoint.v1.WaypointTextStyle toProtobuf(WaypointTextStyle style) {
+ return com.lunarclient.apollo.waypoint.v1.WaypointTextStyle.newBuilder()
+ .setShowText(style.isShowText())
+ .setOnlyShowTextWhenLookingNear(style.isOnlyShowTextWhenLookingNear())
+ .setShowIcons(style.isShowIcons())
+ .setTextIconScale(checkRange(style.getTextIconScale(), 0.1F, 3.0F, "WaypointTextStyle#textIconScale"))
+ .setLabelScale(checkRange(style.getLabelScale(), 0.1F, 2.0F, "WaypointTextStyle#labelScale"))
+ .setBoxPadding(checkRange(style.getBoxPadding(), 1.0F, 8.0F, "WaypointTextStyle#boxPadding"))
+ .setBoxBorders(style.isBoxBorders())
+ .setTextShadow(style.isTextShadow())
+ .setShowDistance(style.isShowDistance())
.build();
}
private static final class WaypointSerializer implements TypeSerializer {
@Override
public Waypoint deserialize(Type type, ConfigurationNode node) throws SerializationException {
- return Waypoint.builder()
+ Waypoint.WaypointBuilder builder = Waypoint.builder()
.name(this.virtualNode(node, "name").getString())
.location(ApolloBlockLocation.builder()
.world(this.virtualNode(node, "location", "world").getString())
.x(this.virtualNode(node, "location", "x").getInt())
.y(this.virtualNode(node, "location", "y").getInt())
.z(this.virtualNode(node, "location", "z").getInt())
- .build()
- )
- .color(Color.decode(this.virtualNode(node, "color").getString("#FFFFFF")))
- .preventRemoval(this.virtualNode(node, "prevent-removal").getBoolean())
- .hidden(this.virtualNode(node, "hidden").getBoolean())
- .build();
+ .build())
+ .color(Color.decode(node.node("color").getString("#FFFFFF")))
+ .preventRemoval(node.node("prevent-removal").getBoolean())
+ .hidden(node.node("hidden").getBoolean());
+
+ if (node.hasChild("show-beam")) {
+ builder.showBeam(node.node("show-beam").getBoolean(true));
+ }
+
+ if (node.hasChild("highlight-block")) {
+ builder.highlightBlock(node.node("highlight-block").getBoolean(true));
+ }
+
+ if (node.hasChild("highlight-block-line-width")) {
+ builder.highlightBlockLineWidth((float) node.node("highlight-block-line-width").getDouble(4.0D));
+ }
+
+ if (node.hasChild("style")) {
+ builder.textStyle(this.readStyle(node.node("style")));
+ }
+
+ return builder.build();
}
@Override
@@ -141,6 +206,66 @@ public void serialize(Type type, @Nullable Waypoint waypoint, ConfigurationNode
node.node("color").set(String.format("#%06X", (0xFFFFFF & waypoint.getColor().getRGB())));
node.node("prevent-removal").set(waypoint.isPreventRemoval());
node.node("hidden").set(waypoint.isHidden());
+ node.node("show-beam").set(waypoint.isShowBeam());
+ node.node("highlight-block").set(waypoint.isHighlightBlock());
+ node.node("highlight-block-line-width").set(waypoint.getHighlightBlockLineWidth());
+
+ if (waypoint.getTextStyle() != null) {
+ this.writeStyle(node.node("style"), waypoint.getTextStyle());
+ }
+ }
+
+ private WaypointTextStyle readStyle(ConfigurationNode node) {
+ WaypointTextStyle.WaypointTextStyleBuilder builder = WaypointTextStyle.builder();
+ if (node.hasChild("show-text")) {
+ builder.showText(node.node("show-text").getBoolean(true));
+ }
+
+ if (node.hasChild("only-show-text-when-looking-near")) {
+ builder.onlyShowTextWhenLookingNear(node.node("only-show-text-when-looking-near").getBoolean(false));
+ }
+
+ if (node.hasChild("show-icons")) {
+ builder.showIcons(node.node("show-icons").getBoolean(false));
+ }
+
+ if (node.hasChild("text-icon-scale")) {
+ builder.textIconScale(node.node("text-icon-scale").getFloat(1.5F));
+ }
+
+ if (node.hasChild("label-scale")) {
+ builder.labelScale(node.node("label-scale").getFloat(1.0F));
+ }
+
+ if (node.hasChild("box-padding")) {
+ builder.boxPadding(node.node("box-padding").getFloat(4.0F));
+ }
+
+ if (node.hasChild("box-borders")) {
+ builder.boxBorders(node.node("box-borders").getBoolean(true));
+ }
+
+ if (node.hasChild("text-shadow")) {
+ builder.textShadow(node.node("text-shadow").getBoolean(false));
+ }
+
+ if (node.hasChild("show-distance")) {
+ builder.showDistance(node.node("show-distance").getBoolean(true));
+ }
+
+ return builder.build();
+ }
+
+ private void writeStyle(ConfigurationNode node, WaypointTextStyle style) throws SerializationException {
+ node.node("show-text").set(style.isShowText());
+ node.node("only-show-text-when-looking-near").set(style.isOnlyShowTextWhenLookingNear());
+ node.node("show-icons").set(style.isShowIcons());
+ node.node("text-icon-scale").set(style.getTextIconScale());
+ node.node("label-scale").set(style.getLabelScale());
+ node.node("box-padding").set(style.getBoxPadding());
+ node.node("box-borders").set(style.isBoxBorders());
+ node.node("text-shadow").set(style.isTextShadow());
+ node.node("show-distance").set(style.isShowDistance());
}
private ConfigurationNode virtualNode(ConfigurationNode source, Object... path) throws SerializationException {
diff --git a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java
index 1bc39264..00cb3c65 100644
--- a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java
+++ b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java
@@ -28,6 +28,7 @@
import com.lunarclient.apollo.common.cuboid.Cuboid2D;
import com.lunarclient.apollo.common.cuboid.Cuboid3D;
import com.lunarclient.apollo.common.icon.AdvancedResourceLocationIcon;
+import com.lunarclient.apollo.common.icon.CustomModelData;
import com.lunarclient.apollo.common.icon.Icon;
import com.lunarclient.apollo.common.icon.ItemStackIcon;
import com.lunarclient.apollo.common.icon.ResourceLocationIcon;
@@ -577,6 +578,11 @@ public static com.lunarclient.apollo.common.v1.ItemStackIcon toProtobuf(ItemStac
.setItemId(icon.getItemId())
.setCustomModelData(icon.getCustomModelData());
+ CustomModelData customModelData = icon.getCustomModelDataObject();
+ if (customModelData != null) {
+ builder.setCustomModelDataObject(NetworkTypes.toProtobuf(customModelData));
+ }
+
if (icon.getItemName() != null) {
builder.setItemName(icon.getItemName());
}
@@ -602,6 +608,10 @@ public static ItemStackIcon fromProtobuf(com.lunarclient.apollo.common.v1.ItemSt
.itemId(icon.getItemId())
.customModelData(icon.getCustomModelData());
+ if (icon.hasCustomModelDataObject()) {
+ builder.customModelDataObject(NetworkTypes.fromProtobuf(icon.getCustomModelDataObject()));
+ }
+
if (icon.hasProfile()) {
builder.profile(NetworkTypes.fromProtobuf(icon.getProfile()));
}
@@ -609,6 +619,40 @@ public static ItemStackIcon fromProtobuf(com.lunarclient.apollo.common.v1.ItemSt
return builder.build();
}
+ /**
+ * Converts a {@link CustomModelData} object to a
+ * {@link com.lunarclient.apollo.common.v1.CustomModelData} proto message.
+ *
+ * @param object the custom model data
+ * @return the proto custom model data message
+ * @since 1.2.7
+ */
+ public static com.lunarclient.apollo.common.v1.CustomModelData toProtobuf(CustomModelData object) {
+ return com.lunarclient.apollo.common.v1.CustomModelData.newBuilder()
+ .addAllFloats(object.getFloats())
+ .addAllFlags(object.getFlags())
+ .addAllStrings(object.getStrings())
+ .addAllColors(object.getColors())
+ .build();
+ }
+
+ /**
+ * Converts a {@link com.lunarclient.apollo.common.v1.CustomModelData}
+ * proto message to a {@link CustomModelData} object.
+ *
+ * @param message the custom model data message
+ * @return the custom model data object
+ * @since 1.2.7
+ */
+ public static CustomModelData fromProtobuf(com.lunarclient.apollo.common.v1.CustomModelData message) {
+ return CustomModelData.builder()
+ .floats(message.getFloatsList())
+ .flags(message.getFlagsList())
+ .strings(message.getStringsList())
+ .colors(message.getColorsList())
+ .build();
+ }
+
/**
* Converts a {@link Profile} object to a
* {@link com.lunarclient.apollo.common.v1.Profile} proto message.
diff --git a/docs/developers/lightweight/json/object-util.mdx b/docs/developers/lightweight/json/object-util.mdx
index f60dbbfd..91d086f2 100644
--- a/docs/developers/lightweight/json/object-util.mdx
+++ b/docs/developers/lightweight/json/object-util.mdx
@@ -88,11 +88,11 @@ public static JsonObject createBlockLocationObject(@NotNull Location location) {
Icon-related methods
```java
-public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData) {
- return JsonUtil.createItemStackIconObject(itemName, itemId, customModelData, null);
+public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId) {
+ return JsonUtil.createItemStackIconObject(itemName, itemId, null, null);
}
-public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData, @Nullable JsonObject profile) {
+public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, @Nullable JsonObject customModelData, @Nullable JsonObject profile) {
JsonObject itemIconObject = new JsonObject();
if (itemName != null) {
itemIconObject.addProperty("item_name", itemName);
@@ -100,7 +100,9 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in
itemIconObject.addProperty("item_id", itemId);
}
- itemIconObject.addProperty("custom_model_data", customModelData);
+ if (customModelData != null) {
+ itemIconObject.add("custom_model_data_object", customModelData);
+ }
if (profile != null) {
itemIconObject.add("profile", profile);
@@ -111,6 +113,28 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in
return iconObject;
}
+public static JsonObject createCustomModelDataObject(List floats, List flags, List strings, List colors) {
+ JsonObject customModelDataObject = new JsonObject();
+
+ JsonArray floatsArray = new JsonArray();
+ floats.forEach(floatsArray::add);
+ customModelDataObject.add("floats", floatsArray);
+
+ JsonArray flagsArray = new JsonArray();
+ flags.forEach(flagsArray::add);
+ customModelDataObject.add("flags", flagsArray);
+
+ JsonArray stringsArray = new JsonArray();
+ strings.forEach(stringsArray::add);
+ customModelDataObject.add("strings", stringsArray);
+
+ JsonArray colorsArray = new JsonArray();
+ colors.forEach(colorsArray::add);
+ customModelDataObject.add("colors", colorsArray);
+
+ return customModelDataObject;
+}
+
public static JsonObject createProfileObject(@Nullable UUID id, @NotNull String texture, @NotNull String signature) {
JsonObject profileObject = new JsonObject();
if (id != null) {
diff --git a/docs/developers/lightweight/json/packet-util.mdx b/docs/developers/lightweight/json/packet-util.mdx
index cbb8ef03..0dd5b73e 100644
--- a/docs/developers/lightweight/json/packet-util.mdx
+++ b/docs/developers/lightweight/json/packet-util.mdx
@@ -33,6 +33,7 @@ private static final Table CONFIG_MODULE_PROPERTIES = Ha
static {
// Module Options the client needs to be notified about. These properties are sent with the enable module packet.
// While using the Apollo plugin this would be equivalent to modifying the config.yml
+ CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", false);
CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", false);
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", false);
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", false);
diff --git a/docs/developers/lightweight/protobuf.mdx b/docs/developers/lightweight/protobuf.mdx
index c1129b9d..0d43206e 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.1.6
+ 0.1.7
```
@@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B
}
dependencies {
- api 'com.lunarclient:apollo-protos:0.1.6'
+ api 'com.lunarclient:apollo-protos:0.1.7'
}
```
@@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B
}
dependencies {
- api("com.lunarclient:apollo-protos:0.1.6")
+ api("com.lunarclient:apollo-protos:0.1.7")
}
```
diff --git a/docs/developers/lightweight/protobuf/object-util.mdx b/docs/developers/lightweight/protobuf/object-util.mdx
index 7f5817b7..a7d410f1 100644
--- a/docs/developers/lightweight/protobuf/object-util.mdx
+++ b/docs/developers/lightweight/protobuf/object-util.mdx
@@ -97,19 +97,22 @@ public static Location toBukkitPlayerLocation(JsonObject message) {
Icon-related methods
```java
-public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData) {
- return ProtobufUtil.createItemStackIconProto(itemName, itemId, customModelData, null);
+public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId) {
+ return ProtobufUtil.createItemStackIconProto(itemName, itemId, null, null);
}
-public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData, @Nullable Profile profile) {
+public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, @Nullable CustomModelData customModelData, @Nullable Profile profile) {
ItemStackIcon.Builder iconBuilder = ItemStackIcon.newBuilder()
- .setItemId(itemId)
- .setCustomModelData(customModelData);
+ .setItemId(itemId);
if (itemName != null) {
iconBuilder.setItemName(itemName);
}
+ if (customModelData != null) {
+ iconBuilder.setCustomModelDataObject(customModelData);
+ }
+
if (profile != null) {
iconBuilder.setProfile(profile);
}
@@ -117,6 +120,15 @@ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName,
return iconBuilder.build();
}
+public static CustomModelData createCustomModelDataProto(List floats, List flags, List strings, List colors) {
+ return CustomModelData.newBuilder()
+ .addAllFloats(floats)
+ .addAllFlags(flags)
+ .addAllStrings(strings)
+ .addAllColors(colors)
+ .build();
+}
+
public static Profile createProfileProto(@Nullable UUID id, String texture, String signature) {
Profile.Builder builder = Profile.newBuilder()
.setTexture(texture)
diff --git a/docs/developers/lightweight/protobuf/packet-util.mdx b/docs/developers/lightweight/protobuf/packet-util.mdx
index bca907ab..7290d308 100644
--- a/docs/developers/lightweight/protobuf/packet-util.mdx
+++ b/docs/developers/lightweight/protobuf/packet-util.mdx
@@ -33,6 +33,7 @@ private static final Table CONFIG_MODULE_PROPERTIES = Has
static {
// Module Options the client needs to be notified about. These properties are sent with the enable module packet.
// While using the Apollo plugin this would be equivalent to modifying the config.yml
+ CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", Value.newBuilder().setBoolValue(false).build());
diff --git a/docs/developers/modules/cooldown.mdx b/docs/developers/modules/cooldown.mdx
index 6c695fea..55fe092d 100644
--- a/docs/developers/modules/cooldown.mdx
+++ b/docs/developers/modules/cooldown.mdx
@@ -216,7 +216,7 @@ public void displayCooldownItemExample(Player viewer) {
.setName("enderpearl-cooldown")
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15)))
.setIcon(Icon.newBuilder()
- .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_PEARL", 0, 0))
+ .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_PEARL", 0))
.build())
.build();
@@ -232,7 +232,7 @@ public void displayCooldownWithStyleExample(Player viewer) {
.setName("book-cooldown")
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(30)))
.setIcon(Icon.newBuilder()
- .setItemStack(ProtobufUtil.createItemStackIconProto("BOOK", 0, 0))
+ .setItemStack(ProtobufUtil.createItemStackIconProto("BOOK", 0))
.build())
.setStyle(CooldownStyle.newBuilder()
.setCircleStartColor(ProtobufUtil.createColorProto(new Color(255, 85, 85))) // ApolloColors.RED
@@ -255,7 +255,7 @@ public void displayCooldownWithPlayerTextureExample(Player viewer) {
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15)))
.setIcon(Icon.newBuilder()
.setItemStack(ProtobufUtil.createItemStackIconProto(
- "PLAYER_HEAD", 0, 0,
+ "PLAYER_HEAD", 0, null,
ProtobufUtil.createProfileProto(
UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"),
"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19",
@@ -322,7 +322,7 @@ public void displayCooldownItemExample(Player viewer) {
message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage");
message.addProperty("name", "enderpearl-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15)));
- message.add("icon", JsonUtil.createItemStackIconObject("ENDER_PEARL", 0, 0));
+ message.add("icon", JsonUtil.createItemStackIconObject("ENDER_PEARL", 0));
JsonPacketUtil.sendPacket(viewer, message);
}
@@ -336,7 +336,7 @@ public void displayCooldownWithStyleExample(Player viewer) {
message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage");
message.addProperty("name", "book-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(30)));
- message.add("icon", JsonUtil.createItemStackIconObject("BOOK", 0, 0));
+ message.add("icon", JsonUtil.createItemStackIconObject("BOOK", 0));
JsonObject style = new JsonObject();
style.add("circle_start_color", JsonUtil.createColorObject(new Color(255, 85, 85))); // ApolloColors.RED
@@ -358,7 +358,7 @@ public void displayCooldownWithPlayerTextureExample(Player viewer) {
message.addProperty("name", "player-head-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15)));
message.add("icon", JsonUtil.createItemStackIconObject(
- "PLAYER_HEAD", 0, 0,
+ "PLAYER_HEAD", 0, null,
JsonUtil.createProfileObject(
UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"),
"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19",
diff --git a/docs/developers/modules/waypoint.mdx b/docs/developers/modules/waypoint.mdx
index 6a541b60..d693cd68 100644
--- a/docs/developers/modules/waypoint.mdx
+++ b/docs/developers/modules/waypoint.mdx
@@ -12,6 +12,9 @@ The waypoint module allows you to interact with the Waypoints mod in Lunar Clien
- Supply a custom name for the waypoint.
- Control the ability to remove the waypoint. (They will always have the option to disable the waypoint, even if you prevent it from being deleted client-side.)
- Control if the waypoint is set to be hidden by default.
+ - Toggle the beam and the block outline highlight.
+ - Override the on-screen label, icons, scaling, padding, borders, text shadow and distance HUD per waypoint.
+ - Show or hide an existing waypoint without removing it.

@@ -57,6 +60,38 @@ public void displayWaypointExample(Player viewer) {
}
```
+### Displaying a Waypoint with Text Style
+
+Attach a `WaypointTextStyle` to override the on-screen label, icons, scaling and distance HUD on a per-waypoint basis. Any field you leave at its default keeps deferring to the player's own Waypoints mod settings — see the [`Waypoint` Options](#waypoint-options) section below for the full list and accepted ranges.
+
+```java
+public void displayWaypointWithTextStyle(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+
+ apolloPlayerOpt.ifPresent(apolloPlayer -> {
+ this.waypointModule.displayWaypoint(apolloPlayer, Waypoint.builder()
+ .name("POI")
+ .location(ApolloBlockLocation.builder()
+ .world("world")
+ .x(-500)
+ .y(100)
+ .z(-500)
+ .build()
+ )
+ .color(Color.GREEN)
+ .preventRemoval(false)
+ .hidden(false)
+ .textStyle(WaypointTextStyle.builder()
+ .showIcons(true)
+ .labelScale(0.5F)
+ .textIconScale(0.7F)
+ .build())
+ .build()
+ );
+ });
+}
+```
+
### Removing a Waypoint
```java
@@ -66,6 +101,22 @@ public void removeWaypointExample(Player viewer) {
}
```
+### Showing or Hiding an Existing Waypoint
+
+Use these to toggle visibility of a waypoint already sent to the client, without removing it.
+
+```java
+public void showWaypointExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.waypointModule.showWaypoint(apolloPlayer, "KoTH"));
+}
+
+public void hideWaypointExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+ apolloPlayerOpt.ifPresent(apolloPlayer -> this.waypointModule.hideWaypoint(apolloPlayer, "KoTH"));
+}
+```
+
### Resetting all Waypoints
```java
@@ -143,6 +194,41 @@ Custom colors can be created from any RGB values using `new Color(int red, int g
.hidden(false)
```
+`.showBeam(Boolean)` whether the beacon beam for this waypoint is drawn. Defaults to `true`.
+
+```java
+.showBeam(true)
+```
+
+`.highlightBlock(Boolean)` whether the in-world block outline highlight at the waypoint location is drawn. Defaults to `true`.
+
+```java
+.highlightBlock(true)
+```
+
+`.highlightBlockLineWidth(float)` line width of the block highlight outline when `highlightBlock` is enabled. Accepts `1.5F` to `7.5F`. Leaving the field at its default of `0.0F` is treated as a fallback to the player's own configured line width.
+
+```java
+.highlightBlockLineWidth(5.0F)
+```
+
+`.textStyle(WaypointTextStyle)` overrides for the on-screen label, icons and distance HUD. Leave unset (or `null`) to defer entirely to the player's own Waypoints mod settings.
+
+```java
+.textStyle(WaypointTextStyle.builder()
+ .showText(true)
+ .onlyShowTextWhenLookingNear(false)
+ .showIcons(false)
+ .textIconScale(1.5F) // 0.1F to 3.0F
+ .labelScale(1.0F) // 0.1F to 2.0F
+ .boxPadding(4.0F) // 1.0F to 8.0F
+ .boxBorders(true)
+ .textShadow(false)
+ .showDistance(true)
+ .build()
+)
+```
+
@@ -171,6 +257,27 @@ public void displayWaypointExample(Player viewer) {
}
```
+**Displaying a Waypoint with Text Style**
+
+```java
+public void displayWaypointWithTextStyle(Player viewer) {
+ DisplayWaypointMessage message = DisplayWaypointMessage.newBuilder()
+ .setName("POI")
+ .setLocation(ProtobufUtil.createBlockLocationProto(new Location(viewer.getWorld(), -500, 100, -500)))
+ .setColor(ProtobufUtil.createColorProto(Color.GREEN))
+ .setPreventRemoval(false)
+ .setHidden(false)
+ .setStyle(WaypointTextStyle.newBuilder()
+ .setShowIcons(true)
+ .setLabelScale(0.5F)
+ .setTextIconScale(0.7F)
+ .build())
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+```
+
**Removing a Waypoint**
```java
@@ -183,6 +290,26 @@ public void removeWaypointExample(Player viewer) {
}
```
+**Showing or Hiding an Existing Waypoint**
+
+```java
+public void showWaypointExample(Player viewer) {
+ ShowWaypointMessage message = ShowWaypointMessage.newBuilder()
+ .setName("KoTH")
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+
+public void hideWaypointExample(Player viewer) {
+ HideWaypointMessage message = HideWaypointMessage.newBuilder()
+ .setName("KoTH")
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+}
+```
+
**Resetting all Waypoints**
```java
@@ -222,6 +349,30 @@ public void displayWaypointExample(Player viewer) {
}
```
+**Displaying a Waypoint with Text Style**
+
+```java
+public void displayWaypointWithTextStyle(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.waypoint.v1.DisplayWaypointMessage");
+ message.addProperty("name", "POI");
+ message.add("location", JsonUtil.createBlockLocationObject(
+ new Location(Bukkit.getWorld("world"), -500, 100, -500)
+ ));
+ message.add("color", JsonUtil.createColorObject(Color.GREEN));
+ message.addProperty("prevent_removal", false);
+ message.addProperty("hidden", false);
+
+ JsonObject style = new JsonObject();
+ style.addProperty("show_icons", true);
+ style.addProperty("label_scale", 0.5F);
+ style.addProperty("text_icon_scale", 0.7F);
+ message.add("style", style);
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+```
+
**Removing a Waypoint**
```java
@@ -234,6 +385,26 @@ public void removeWaypointExample(Player viewer) {
}
```
+**Showing or Hiding an Existing Waypoint**
+
+```java
+public void showWaypointExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.waypoint.v1.ShowWaypointMessage");
+ message.addProperty("name", "KoTH");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+
+public void hideWaypointExample(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.waypoint.v1.HideWaypointMessage");
+ message.addProperty("name", "KoTH");
+
+ JsonPacketUtil.sendPacket(viewer, message);
+}
+```
+
**Resetting all Waypoints**
```java
diff --git a/docs/developers/utilities/icons.mdx b/docs/developers/utilities/icons.mdx
index 1b1b488a..28eba917 100644
--- a/docs/developers/utilities/icons.mdx
+++ b/docs/developers/utilities/icons.mdx
@@ -35,12 +35,12 @@ public final class ItemStackIcon extends Icon {
int itemId;
/**
- * Returns the icon {@link Integer} custom model data.
+ * Returns the icon {@link CustomModelData} object.
*
- * @return the icon custom model data
- * @since 1.0.7
+ * @return the icon custom model data object
+ * @since 1.2.7
*/
- int customModelData;
+ @Nullable CustomModelData customModelDataObject;
/**
* Returns the icon {@link Profile}.
@@ -53,6 +53,48 @@ public final class ItemStackIcon extends Icon {
}
```
+## `CustomModelData` Builder
+
+The `CustomModelData` builder mirrors the Minecraft 1.21.4+ custom model data object.
+
+```java
+public final class CustomModelData {
+
+ /**
+ * Returns the custom model data {@link Float} values.
+ *
+ * @return the custom model data floats
+ * @since 1.2.7
+ */
+ List floats;
+
+ /**
+ * Returns the custom model data {@link Boolean} flags.
+ *
+ * @return the custom model data flags
+ * @since 1.2.7
+ */
+ List flags;
+
+ /**
+ * Returns the custom model data {@link String} values.
+ *
+ * @return the custom model data strings
+ * @since 1.2.7
+ */
+ List strings;
+
+ /**
+ * Returns the custom model data color {@link Integer} values.
+ *
+ * @return the custom model data colors
+ * @since 1.2.7
+ */
+ List colors;
+
+}
+```
+
### Sample Code
```java
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 0949ac90..d0cb1938 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
@@ -32,10 +32,10 @@
import com.lunarclient.apollo.example.module.impl.CooldownExample;
import com.lunarclient.apollo.example.module.impl.CosmeticExample;
import com.lunarclient.apollo.example.module.impl.ServerLinkExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
import com.lunarclient.apollo.example.nms.NpcManager;
import com.lunarclient.apollo.example.nms.PlayerNpc;
import com.lunarclient.apollo.player.ApolloPlayer;
-import java.util.Optional;
import org.bukkit.entity.Player;
public class ApolloPlayerApiListener implements ApolloListener {
@@ -64,7 +64,6 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) {
this.example.getBeamExample().displayBeamExample(player);
this.example.getBorderExample().displayBorderExample(player);
- this.example.getWaypointExample().displayWaypointExample(player);
CooldownExample cooldownExample = this.example.getCooldownExample();
cooldownExample.displayCooldownItemExample(player);
@@ -79,8 +78,11 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) {
CosmeticExample cosmeticExample = this.example.getCosmeticExample();
NpcManager npcManager = this.example.getNpcManager();
- Optional npc = npcManager.findByName("Apollo");
- npc.ifPresent(playerNpc -> cosmeticExample.equipNpcCosmeticsExample(player, playerNpc.getUuid()));
+ for (PlayerNpc npc : npcManager.getNpcs()) {
+ for (CommandCosmetic spec : npc.getCosmetics()) {
+ cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec);
+ }
+ }
}
}
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 df3f8c38..3e500a0c 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
@@ -27,10 +27,14 @@
import com.lunarclient.apollo.Apollo;
import com.lunarclient.apollo.common.location.ApolloBlockLocation;
import com.lunarclient.apollo.example.module.impl.CosmeticExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
import com.lunarclient.apollo.module.cosmetic.Cosmetic;
import com.lunarclient.apollo.module.cosmetic.CosmeticModule;
import com.lunarclient.apollo.module.cosmetic.Spray;
+import com.lunarclient.apollo.module.cosmetic.options.BodyOptions;
import com.lunarclient.apollo.module.cosmetic.options.CloakOptions;
+import com.lunarclient.apollo.module.cosmetic.options.CosmeticOptions;
+import com.lunarclient.apollo.module.cosmetic.options.HatOptions;
import com.lunarclient.apollo.module.cosmetic.options.PetOptions;
import com.lunarclient.apollo.module.packetenrichment.raytrace.Direction;
import com.lunarclient.apollo.player.ApolloPlayer;
@@ -87,6 +91,54 @@ public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List
this.cosmeticModule.equipNpcCosmetics(Recipients.ofEveryone(), npcUuid, cosmetics);
}
+ @Override
+ public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic spec) {
+ this.cosmeticModule.equipNpcCosmetics(Recipients.ofEveryone(), npcUuid, Lists.newArrayList(this.toApiCosmetic(spec)));
+ }
+
+ @Override
+ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic spec) {
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer ->
+ this.cosmeticModule.equipNpcCosmetics(apolloPlayer, npcUuid, Lists.newArrayList(this.toApiCosmetic(spec))));
+ }
+
+ private Cosmetic toApiCosmetic(CommandCosmetic spec) {
+ return Cosmetic.builder()
+ .id(spec.getId())
+ .options(this.toApiOptions(spec.getOptions()))
+ .build();
+ }
+
+ private CosmeticOptions toApiOptions(CommandCosmetic.Options options) {
+ if (options instanceof CommandCosmetic.Hat) {
+ CommandCosmetic.Hat hat = (CommandCosmetic.Hat) options;
+ return HatOptions.builder()
+ .showOverHelmet(hat.isShowOverHelmet())
+ .showOverSkinLayer(hat.isShowOverSkinLayer())
+ .heightOffset(hat.getHeightOffset())
+ .build();
+ } else if (options instanceof CommandCosmetic.Cloak) {
+ CommandCosmetic.Cloak cloak = (CommandCosmetic.Cloak) options;
+ return CloakOptions.builder()
+ .useClothPhysics(cloak.isUseClothPhysics())
+ .build();
+ } else if (options instanceof CommandCosmetic.Pet) {
+ CommandCosmetic.Pet pet = (CommandCosmetic.Pet) options;
+ return PetOptions.builder()
+ .flipShoulder(pet.isFlipShoulder())
+ .build();
+ } else if (options instanceof CommandCosmetic.Body) {
+ CommandCosmetic.Body body = (CommandCosmetic.Body) options;
+ return BodyOptions.builder()
+ .showOverChestplate(body.isShowOverChestplate())
+ .showOverLeggings(body.isShowOverLeggings())
+ .showOverBoots(body.isShowOverBoots())
+ .build();
+ }
+
+ return null;
+ }
+
@Override
public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) {
Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/WaypointApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/WaypointApiExample.java
index 2e7225c0..7396e9c1 100644
--- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/WaypointApiExample.java
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/WaypointApiExample.java
@@ -28,6 +28,7 @@
import com.lunarclient.apollo.example.module.impl.WaypointExample;
import com.lunarclient.apollo.module.waypoint.Waypoint;
import com.lunarclient.apollo.module.waypoint.WaypointModule;
+import com.lunarclient.apollo.module.waypoint.WaypointTextStyle;
import com.lunarclient.apollo.player.ApolloPlayer;
import java.awt.Color;
import java.util.Optional;
@@ -59,6 +60,33 @@ public void displayWaypointExample(Player viewer) {
});
}
+ @Override
+ public void displayWaypointWithTextStyle(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+
+ apolloPlayerOpt.ifPresent(apolloPlayer -> {
+ this.waypointModule.displayWaypoint(apolloPlayer, Waypoint.builder()
+ .name("POI")
+ .location(ApolloBlockLocation.builder()
+ .world("world") // The world the waypoint should display in
+ .x(-500)
+ .y(100)
+ .z(-500)
+ .build()
+ )
+ .color(Color.GREEN)
+ .preventRemoval(false) // If the player can delete the waypoint
+ .hidden(false)
+ .textStyle(WaypointTextStyle.builder()
+ .showIcons(true)
+ .labelScale(0.5F)
+ .textIconScale(0.7F)
+ .build())
+ .build()
+ );
+ });
+ }
+
@Override
public void removeWaypointExample(Player viewer) {
Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml
index 2549361d..ec36acb2 100644
--- a/example/bukkit/api/src/main/resources/plugin.yml
+++ b/example/bukkit/api/src/main/resources/plugin.yml
@@ -9,6 +9,7 @@ folia-supported: true
commands:
npc:
description: "NPCs!"
+ aliases: [apollonpc]
apollodebug:
description: "Apollo Debug!"
modstatus:
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 b524b605..9d29803e 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
@@ -152,7 +152,7 @@ public void onEnable() {
@Override
public void onDisable() {
if (this.npcManager != null) {
- this.npcManager.removeAll();
+ this.npcManager.despawnAll();
}
}
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 fb88c1b5..d970d1d8 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
@@ -25,11 +25,17 @@
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.util.CommandUtil;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.UUID;
+import java.util.stream.Collectors;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@@ -82,8 +88,16 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
switch (args[0].toLowerCase()) {
case "equip": {
+ if (args.length >= 3 && this.isCosmeticType(args[2])) {
+ this.handleTypedEquip(player, example, uuid, npcName, args);
+ break;
+ }
+
List cosmeticIds = this.parseCosmeticIds(args);
example.equipNpcCosmeticsInternal(player, uuid, cosmeticIds);
+ this.persistEquipped(uuid, cosmeticIds.stream()
+ .map(id -> CommandCosmetic.builder().id(id).build())
+ .collect(Collectors.toList()));
player.sendMessage(ChatColor.GREEN + "Equipped cosmetics " + cosmeticIds + " on NPC " + npcName);
break;
}
@@ -91,12 +105,14 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
case "unequip": {
List cosmeticIds = this.parseCosmeticIds(args);
example.unequipNpcCosmeticsInternal(player, uuid, cosmeticIds);
+ this.persistUnequipped(uuid, cosmeticIds);
player.sendMessage(ChatColor.GREEN + "Unequipped cosmetics " + cosmeticIds + " from NPC " + npcName);
break;
}
case "reset": {
example.resetNpcCosmeticsExample(player, uuid);
+ this.persistReset(uuid);
player.sendMessage(ChatColor.GREEN + "Reset all cosmetics on NPC " + npcName);
break;
}
@@ -170,6 +186,125 @@ private boolean handleSpray(Player player, CosmeticExample example, String[] arg
return true;
}
+ private boolean isCosmeticType(String type) {
+ String lower = type.toLowerCase();
+ return "hat".equals(lower) || "cloak".equals(lower) || "pet".equals(lower) || "body".equals(lower);
+ }
+
+ private void handleTypedEquip(Player player, CosmeticExample example, UUID uuid, String npcName, String[] args) {
+ if (args.length < 4) {
+ this.sendUsage(player);
+ return;
+ }
+
+ int cosmeticId;
+ try {
+ cosmeticId = Integer.parseInt(args[3]);
+ } catch (NumberFormatException ex) {
+ player.sendMessage(ChatColor.RED + "Cosmetic id must be an integer.");
+ return;
+ }
+
+ Map options = this.parseOptions(args, 4);
+ String type = args[2].toLowerCase();
+
+ CommandCosmetic.Options optionsSpec;
+ switch (type) {
+ case "hat": {
+ optionsSpec = CommandCosmetic.Hat.builder()
+ .showOverHelmet(CommandUtil.parseBoolean(options.get("showoverhelmet"), true))
+ .showOverSkinLayer(CommandUtil.parseBoolean(options.get("showoverskinlayer"), true))
+ .heightOffset(CommandUtil.parseFloat(options.get("heightoffset"), 0f))
+ .build();
+ break;
+ }
+ case "cloak": {
+ optionsSpec = CommandCosmetic.Cloak.builder()
+ .useClothPhysics(CommandUtil.parseBoolean(options.get("useclothphysics"), false))
+ .build();
+ break;
+ }
+ case "pet": {
+ optionsSpec = CommandCosmetic.Pet.builder()
+ .flipShoulder(CommandUtil.parseBoolean(options.get("flipshoulder"), false))
+ .build();
+ break;
+ }
+ case "body": {
+ optionsSpec = CommandCosmetic.Body.builder()
+ .showOverChestplate(CommandUtil.parseBoolean(options.get("showoverchestplate"), true))
+ .showOverLeggings(CommandUtil.parseBoolean(options.get("showoverleggings"), true))
+ .showOverBoots(CommandUtil.parseBoolean(options.get("showoverboots"), true))
+ .build();
+ break;
+ }
+ default: {
+ this.sendUsage(player);
+ return;
+ }
+ }
+
+ CommandCosmetic spec = CommandCosmetic.builder()
+ .id(cosmeticId)
+ .options(optionsSpec)
+ .build();
+
+ example.equipNpcCosmeticInternal(player, uuid, spec);
+ this.persistEquipped(uuid, Collections.singletonList(spec));
+ player.sendMessage(ChatColor.GREEN + "Equipped " + type + " cosmetic " + cosmeticId + " on NPC " + npcName);
+ }
+
+ private void persistEquipped(UUID npcUuid, List equipped) {
+ Optional npcOpt = ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(npcUuid);
+ if (!npcOpt.isPresent()) {
+ return;
+ }
+
+ List cosmetics = npcOpt.get().getCosmetics();
+ for (CommandCosmetic cosmetic : equipped) {
+ cosmetics.removeIf(existing -> existing.getId() == cosmetic.getId());
+ cosmetics.add(cosmetic);
+ }
+
+ ApolloExamplePlugin.getInstance().getNpcManager().save();
+ }
+
+ private void persistUnequipped(UUID npcUuid, List cosmeticIds) {
+ Optional npcOpt = ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(npcUuid);
+ if (!npcOpt.isPresent()) {
+ return;
+ }
+
+ npcOpt.get().getCosmetics().removeIf(cosmetic -> cosmeticIds.contains(cosmetic.getId()));
+ ApolloExamplePlugin.getInstance().getNpcManager().save();
+ }
+
+ private void persistReset(UUID npcUuid) {
+ Optional npcOpt = ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(npcUuid);
+ if (!npcOpt.isPresent()) {
+ return;
+ }
+
+ npcOpt.get().getCosmetics().clear();
+ ApolloExamplePlugin.getInstance().getNpcManager().save();
+ }
+
+ private Map parseOptions(String[] args, int startIndex) {
+ Map options = new HashMap<>();
+ for (int i = startIndex; i < args.length; i++) {
+ String token = args[i];
+ int index = token.indexOf('=');
+
+ if (index <= 0 || index == token.length() - 1) {
+ continue;
+ }
+
+ options.put(token.substring(0, index).toLowerCase(), token.substring(index + 1));
+ }
+
+ return options;
+ }
+
private List parseCosmeticIds(String[] args) {
List ids = new ArrayList<>();
for (int i = 2; i < args.length; i++) {
@@ -184,8 +319,26 @@ private List parseCosmeticIds(String[] args) {
private void sendUsage(Player player) {
player.sendMessage("Usage:");
player.sendMessage(" - /cosmetic equip [cosmeticIds]");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic equip Apollo 434 3654 3977");
+ player.sendMessage("");
+ player.sendMessage(" - /cosmetic equip hat ");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic equip Apollo hat 434 showOverHelmet=true showOverSkinLayer=true heightOffset=0.0");
+ player.sendMessage("");
+ player.sendMessage(" - /cosmetic equip cloak ");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic equip Apollo cloak 3 useClothPhysics=true");
+ player.sendMessage("");
+ player.sendMessage(" - /cosmetic equip pet ");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic equip Apollo pet 5095 flipShoulder=true");
+ player.sendMessage("");
+ player.sendMessage(" - /cosmetic equip body ");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic equip Apollo body 3977 showOverChestplate=true showOverLeggings=true showOverBoots=true");
+ player.sendMessage("");
player.sendMessage(" - /cosmetic unequip [cosmeticIds]");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic unequip Apollo 434 3654");
+ player.sendMessage("");
player.sendMessage(" - /cosmetic reset ");
+ player.sendMessage(ChatColor.ITALIC + " /cosmetic reset Apollo");
+ player.sendMessage("");
this.sendSprayUsage(player);
}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java
index 1f03492a..b5ea39bf 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/NpcCommand.java
@@ -86,17 +86,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
break;
}
- case "reset": {
- if (npcManager.getNpcs().isEmpty()) {
- player.sendMessage(ChatColor.YELLOW + "There are no NPCs to remove.");
- break;
- }
-
- npcManager.removeAll();
- player.sendMessage(ChatColor.GREEN + "Removed all tracked NPCs.");
- break;
- }
-
default: {
this.sendUsage(player);
break;
@@ -109,7 +98,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
private void sendUsage(Player player) {
player.sendMessage("Usage: /npc spawn ");
player.sendMessage("Usage: /npc remove ");
- player.sendMessage("Usage: /npc reset");
}
}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/WaypointCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/WaypointCommand.java
index 6970074c..4a24460b 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/WaypointCommand.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/WaypointCommand.java
@@ -43,7 +43,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
Player player = (Player) sender;
if (args.length != 1) {
- player.sendMessage("Usage: /waypoint ");
+ player.sendMessage("Usage: /waypoint ");
return true;
}
@@ -56,6 +56,12 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
break;
}
+ case "displaywithstyle": {
+ waypointExample.displayWaypointWithTextStyle(player);
+ player.sendMessage("Displaying waypoint with style....");
+ break;
+ }
+
case "remove": {
waypointExample.removeWaypointExample(player);
player.sendMessage("Removing waypoint....");
@@ -68,7 +74,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
}
default: {
- player.sendMessage("Usage: /waypoint ");
+ player.sendMessage("Usage: /waypoint ");
break;
}
}
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 d997de77..2a060b20 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
@@ -24,6 +24,8 @@
package com.lunarclient.apollo.example.module.impl;
import com.lunarclient.apollo.example.module.ApolloModuleExample;
+import com.lunarclient.apollo.example.nms.CommandCosmetic;
+import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.bukkit.entity.Player;
@@ -34,6 +36,14 @@ public abstract class CosmeticExample extends ApolloModuleExample {
public abstract void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds);
+ public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ this.equipNpcCosmeticsInternal(viewer, npcUuid, Collections.singletonList(cosmetic.getId()));
+ }
+
+ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) {
+ this.equipNpcCosmeticInternal(viewer, npcUuid, cosmetic);
+ }
+
public abstract void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid);
public abstract void unequipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List cosmeticIds);
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/WaypointExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/WaypointExample.java
index 86ae902e..3450ad4f 100644
--- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/WaypointExample.java
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/WaypointExample.java
@@ -30,6 +30,8 @@ public abstract class WaypointExample extends ApolloModuleExample {
public abstract void displayWaypointExample(Player viewer);
+ public abstract void displayWaypointWithTextStyle(Player viewer);
+
public abstract void removeWaypointExample(Player viewer);
public abstract void resetWaypointsExample(Player viewer);
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/CommandUtil.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/CommandUtil.java
new file mode 100644
index 00000000..60d1fc84
--- /dev/null
+++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/CommandUtil.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+public final class CommandUtil {
+
+ public static boolean parseBoolean(String value, boolean fallback) {
+ return value != null ? Boolean.parseBoolean(value) : fallback;
+ }
+
+ public static float parseFloat(String value, float fallback) {
+ if (value == null) {
+ return fallback;
+ }
+
+ try {
+ return Float.parseFloat(value);
+ } catch (NumberFormatException ex) {
+ return fallback;
+ }
+ }
+
+ private CommandUtil() {
+ }
+
+}
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java
index 2b853532..22a04409 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CooldownJsonExample.java
@@ -40,7 +40,7 @@ public void displayCooldownItemExample(Player viewer) {
message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage");
message.addProperty("name", "enderpearl-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15)));
- message.add("icon", JsonUtil.createItemStackIconObject("ENDER_PEARL", 0, 0));
+ message.add("icon", JsonUtil.createItemStackIconObject("ENDER_PEARL", 0));
JsonPacketUtil.sendPacket(viewer, message);
}
@@ -51,7 +51,7 @@ public void displayCooldownWithStyleExample(Player viewer) {
message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cooldown.v1.DisplayCooldownMessage");
message.addProperty("name", "book-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(30)));
- message.add("icon", JsonUtil.createItemStackIconObject("BOOK", 0, 0));
+ message.add("icon", JsonUtil.createItemStackIconObject("BOOK", 0));
JsonObject style = new JsonObject();
style.add("circle_start_color", JsonUtil.createColorObject(new Color(255, 85, 85))); // ApolloColors.RED
@@ -70,7 +70,7 @@ public void displayCooldownWithPlayerTextureExample(Player viewer) {
message.addProperty("name", "player-head-cooldown");
message.addProperty("duration", JsonUtil.createDurationObject(Duration.ofSeconds(15)));
message.add("icon", JsonUtil.createItemStackIconObject(
- "PLAYER_HEAD", 0, 0,
+ "PLAYER_HEAD", 0, null,
JsonUtil.createProfileObject(
UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"),
"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19",
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/WaypointJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/WaypointJsonExample.java
index a0a2f64c..84f46b9b 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/WaypointJsonExample.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/WaypointJsonExample.java
@@ -49,6 +49,27 @@ public void displayWaypointExample(Player viewer) {
JsonPacketUtil.sendPacket(viewer, message);
}
+ @Override
+ public void displayWaypointWithTextStyle(Player viewer) {
+ JsonObject message = new JsonObject();
+ message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.waypoint.v1.DisplayWaypointMessage");
+ message.addProperty("name", "POI");
+ message.add("location", JsonUtil.createBlockLocationObject(
+ new Location(Bukkit.getWorld("world"), -500, 100, -500)
+ ));
+ message.add("color", JsonUtil.createColorObject(Color.GREEN));
+ message.addProperty("prevent_removal", false);
+ message.addProperty("hidden", false);
+
+ JsonObject style = new JsonObject();
+ style.addProperty("show_icons", true);
+ style.addProperty("label_scale", 0.5F);
+ style.addProperty("text_icon_scale", 0.7F);
+ message.add("style", style);
+
+ JsonPacketUtil.sendPacket(viewer, message);
+ }
+
@Override
public void removeWaypointExample(Player viewer) {
JsonObject message = new JsonObject();
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java
index d6ff7b8f..c2c03685 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java
@@ -51,6 +51,7 @@ public final class JsonPacketUtil {
static {
// Module Options the client needs to be notified about. These properties are sent with the enable module packet.
// While using the Apollo plugin this would be equivalent to modifying the config.yml
+ CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", false);
CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", false);
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", false);
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", false);
diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java
index 694ce2f4..07d452ed 100644
--- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java
+++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonUtil.java
@@ -23,10 +23,12 @@
*/
package com.lunarclient.apollo.example.json.util;
+import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.awt.Color;
import java.time.Duration;
import java.time.Instant;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
@@ -139,11 +141,11 @@ public static Location toBukkitPlayerLocation(JsonObject message) {
return location;
}
- public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData) {
- return JsonUtil.createItemStackIconObject(itemName, itemId, customModelData, null);
+ public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId) {
+ return JsonUtil.createItemStackIconObject(itemName, itemId, null, null);
}
- public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, int customModelData, @Nullable JsonObject profile) {
+ public static JsonObject createItemStackIconObject(@Nullable String itemName, int itemId, @Nullable JsonObject customModelData, @Nullable JsonObject profile) {
JsonObject itemIconObject = new JsonObject();
if (itemName != null) {
itemIconObject.addProperty("item_name", itemName);
@@ -151,7 +153,9 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in
itemIconObject.addProperty("item_id", itemId);
}
- itemIconObject.addProperty("custom_model_data", customModelData);
+ if (customModelData != null) {
+ itemIconObject.add("custom_model_data_object", customModelData);
+ }
if (profile != null) {
itemIconObject.add("profile", profile);
@@ -162,6 +166,28 @@ public static JsonObject createItemStackIconObject(@Nullable String itemName, in
return iconObject;
}
+ public static JsonObject createCustomModelDataObject(List floats, List flags, List strings, List colors) {
+ JsonObject customModelDataObject = new JsonObject();
+
+ JsonArray floatsArray = new JsonArray();
+ floats.forEach(floatsArray::add);
+ customModelDataObject.add("floats", floatsArray);
+
+ JsonArray flagsArray = new JsonArray();
+ flags.forEach(flagsArray::add);
+ customModelDataObject.add("flags", flagsArray);
+
+ JsonArray stringsArray = new JsonArray();
+ strings.forEach(stringsArray::add);
+ customModelDataObject.add("strings", stringsArray);
+
+ JsonArray colorsArray = new JsonArray();
+ colors.forEach(colorsArray::add);
+ customModelDataObject.add("colors", colorsArray);
+
+ return customModelDataObject;
+ }
+
public static JsonObject createProfileObject(@Nullable UUID id, @NotNull String texture, @NotNull String signature) {
JsonObject profileObject = new JsonObject();
if (id != null) {
diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml
index 027ac7db..ba45de5e 100644
--- a/example/bukkit/json/src/main/resources/plugin.yml
+++ b/example/bukkit/json/src/main/resources/plugin.yml
@@ -9,6 +9,7 @@ folia-supported: true
commands:
npc:
description: "NPCs!"
+ aliases: [apollonpc]
autotexthotkey:
description: "Auto Text Hotkey!"
beam:
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java
new file mode 100644
index 00000000..1fed7076
--- /dev/null
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java
@@ -0,0 +1,67 @@
+/*
+ * 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 lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public final class CommandCosmetic {
+
+ int id;
+ Options options;
+
+ public abstract static class Options {
+ }
+
+ @Getter
+ @Builder
+ public static final class Hat extends Options {
+ @Builder.Default boolean showOverHelmet = true;
+ @Builder.Default boolean showOverSkinLayer = true;
+ @Builder.Default float heightOffset = 0f;
+ }
+
+ @Getter
+ @Builder
+ public static final class Cloak extends Options {
+ @Builder.Default boolean useClothPhysics = false;
+ }
+
+ @Getter
+ @Builder
+ public static final class Pet extends Options {
+ @Builder.Default boolean flipShoulder = false;
+ }
+
+ @Getter
+ @Builder
+ public static final class Body extends Options {
+ @Builder.Default boolean showOverChestplate = true;
+ @Builder.Default boolean showOverLeggings = true;
+ @Builder.Default boolean showOverBoots = true;
+ }
+
+}
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java
new file mode 100644
index 00000000..8aa99497
--- /dev/null
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+
+public final class CosmeticOptionsAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public JsonElement serialize(CommandCosmetic.Options options, Type cosmeticType, JsonSerializationContext context) {
+ if (options == null) {
+ return JsonNull.INSTANCE;
+ }
+
+ JsonObject object;
+ String type;
+ if (options instanceof CommandCosmetic.Hat) {
+ object = context.serialize(options, CommandCosmetic.Hat.class).getAsJsonObject();
+ type = "hat";
+ } else if (options instanceof CommandCosmetic.Cloak) {
+ object = context.serialize(options, CommandCosmetic.Cloak.class).getAsJsonObject();
+ type = "cloak";
+ } else if (options instanceof CommandCosmetic.Pet) {
+ object = context.serialize(options, CommandCosmetic.Pet.class).getAsJsonObject();
+ type = "pet";
+ } else if (options instanceof CommandCosmetic.Body) {
+ object = context.serialize(options, CommandCosmetic.Body.class).getAsJsonObject();
+ type = "body";
+ } else {
+ return JsonNull.INSTANCE;
+ }
+
+ object.addProperty("type", type);
+ return object;
+ }
+
+ @Override
+ public CommandCosmetic.Options deserialize(JsonElement json, Type cosmeticType, JsonDeserializationContext context) {
+ if (json == null || json.isJsonNull() || !json.isJsonObject()) {
+ return null;
+ }
+
+ JsonObject object = json.getAsJsonObject();
+ JsonElement element = object.get("type");
+ if (element == null || element.isJsonNull()) {
+ return null;
+ }
+
+ switch (element.getAsString()) {
+ case "hat": return context.deserialize(object, CommandCosmetic.Hat.class);
+ case "cloak": return context.deserialize(object, CommandCosmetic.Cloak.class);
+ case "pet": return context.deserialize(object, CommandCosmetic.Pet.class);
+ case "body": return context.deserialize(object, CommandCosmetic.Body.class);
+ default: return null;
+ }
+ }
+}
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 22f1d0b2..be00b996 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
@@ -67,12 +67,14 @@ public final class NpcManager implements Listener {
private final Map npcs = new HashMap<>();
private final JavaPlugin plugin;
+ private final NpcStore store;
public NpcManager(JavaPlugin plugin) {
this.plugin = plugin;
+ this.store = new NpcStore(plugin);
Bukkit.getPluginManager().registerEvents(this, plugin);
- Bukkit.getScheduler().runTask(plugin, this::spawnDefaultNpcs);
+ Bukkit.getScheduler().runTask(plugin, this::loadOrSpawnDefaults);
}
public void removeNpc(UUID uuid) {
@@ -82,9 +84,10 @@ public void removeNpc(UUID uuid) {
}
this.despawnNpcs(npc);
+ this.store.save(this.npcs.values());
}
- public void removeAll() {
+ public void despawnAll() {
for (PlayerNpc npc : new ArrayList<>(this.npcs.values())) {
this.despawnNpcs(npc);
}
@@ -98,6 +101,14 @@ public Optional findByName(String name) {
.findFirst();
}
+ public Optional findByUuid(UUID uuid) {
+ return Optional.ofNullable(this.npcs.get(uuid));
+ }
+
+ public void save() {
+ this.store.save(this.npcs.values());
+ }
+
public Collection getNpcs() {
return new ArrayList<>(this.npcs.values());
}
@@ -118,11 +129,43 @@ public void onPlayerQuit(PlayerQuitEvent event) {
}
}
+ private void loadOrSpawnDefaults() {
+ if (!this.store.exists()) {
+ this.spawnDefaultNpcs();
+ this.store.save(this.npcs.values());
+ return;
+ }
+
+ for (NpcStore.Entry entry : this.store.load()) {
+ Location location = this.store.toLocation(entry);
+
+ if (location == null) {
+ continue;
+ }
+
+ PlayerNpc npc = this.spawnNpc(entry.getName(), location, entry.getUuid());
+ if (npc != null) {
+ npc.setCosmetics(new ArrayList<>(entry.getCosmetics()));
+ }
+ }
+ }
+
private void spawnDefaultNpcs() {
this.spawnNpc("Apollo", new Location(Bukkit.getWorld("world"), 20.5, 65, 5.5, 90f, 0f));
}
public @Nullable PlayerNpc spawnNpc(String name, Location location) {
+ UUID npcUuid = new UUID(UUID.randomUUID().getMostSignificantBits(), 0L);
+ PlayerNpc npc = this.spawnNpc(name, location, npcUuid);
+
+ if (npc != null) {
+ this.store.save(this.npcs.values());
+ }
+
+ return npc;
+ }
+
+ public @Nullable PlayerNpc spawnNpc(String name, Location location, UUID npcUuid) {
World world = location.getWorld();
if (world == null) {
return null;
@@ -131,7 +174,6 @@ private void spawnDefaultNpcs() {
MinecraftServer server = MinecraftServer.getServer();
ServerLevel level = ((CraftWorld) world).getHandle();
- UUID npcUuid = new UUID(UUID.randomUUID().getMostSignificantBits(), 0L);
GameProfile profile = new GameProfile(npcUuid, name);
ServerPlayer npc = new ServerPlayer(server, level, profile, NpcManager.NPC_CLIENT_INFO);
diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java
new file mode 100644
index 00000000..36635a95
--- /dev/null
+++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java
@@ -0,0 +1,150 @@
+/*
+ * 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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public final class NpcStore {
+
+ private static final String FILE_NAME = "npcs.json";
+ private static final Type ENTRY_LIST_TYPE = new TypeToken>() { }.getType();
+
+ private final JavaPlugin plugin;
+ private final Gson gson;
+ private final File file;
+
+ public NpcStore(JavaPlugin plugin) {
+ this.plugin = plugin;
+
+ this.gson = new GsonBuilder()
+ .setPrettyPrinting()
+ .registerTypeAdapter(CommandCosmetic.Options.class, new CosmeticOptionsAdapter())
+ .create();
+
+ this.file = new File(plugin.getDataFolder(), NpcStore.FILE_NAME);
+
+ File dataFolder = plugin.getDataFolder();
+ if (!dataFolder.exists() && !dataFolder.mkdirs()) {
+ plugin.getLogger().warning("Failed to create plugin data folder at " + dataFolder.getAbsolutePath());
+ }
+ }
+
+ public boolean exists() {
+ return this.file.isFile();
+ }
+
+ public List load() {
+ if (!this.file.isFile()) {
+ return new ArrayList<>();
+ }
+
+ try (Reader reader = Files.newBufferedReader(this.file.toPath(), StandardCharsets.UTF_8)) {
+ List entries = this.gson.fromJson(reader, NpcStore.ENTRY_LIST_TYPE);
+ return entries != null ? entries : new ArrayList<>();
+ } catch (IOException e) {
+ this.plugin.getLogger().warning("Failed to read " + NpcStore.FILE_NAME + ": " + e.getMessage());
+ return new ArrayList<>();
+ }
+ }
+
+ public void save(Collection npcs) {
+ List entries = new ArrayList<>(npcs.size());
+ for (PlayerNpc npc : npcs) {
+ Location location = npc.getLocation();
+ World world = location.getWorld();
+ if (world == null) {
+ continue;
+ }
+
+ entries.add(new Entry(
+ npc.getUuid().toString(), npc.getName(), world.getName(),
+ location.getX(), location.getY(), location.getZ(),
+ location.getYaw(), location.getPitch(),
+ new ArrayList<>(npc.getCosmetics())
+ ));
+ }
+
+ try {
+ Files.createDirectories(this.file.toPath().getParent());
+ } catch (IOException e) {
+ this.plugin.getLogger().warning("Failed to create plugin data folder for " + NpcStore.FILE_NAME + ": " + e.getMessage());
+ return;
+ }
+
+ try (Writer writer = Files.newBufferedWriter(this.file.toPath(), StandardCharsets.UTF_8)) {
+ this.gson.toJson(entries, NpcStore.ENTRY_LIST_TYPE, writer);
+ } catch (IOException e) {
+ this.plugin.getLogger().warning("Failed to write " + NpcStore.FILE_NAME + ": " + e.getMessage());
+ }
+ }
+
+ public Location toLocation(Entry entry) {
+ World world = Bukkit.getWorld(entry.world);
+
+ if (world == null) {
+ return null;
+ }
+
+ return new Location(world, entry.x, entry.y, entry.z, entry.yaw, entry.pitch);
+ }
+
+ @Getter
+ @RequiredArgsConstructor
+ public static final class Entry {
+
+ private final String uuid;
+ private final String name;
+ private final String world;
+ private final double x;
+ private final double y;
+ private final double z;
+ private final float yaw;
+ private final float pitch;
+ private final List cosmetics;
+
+ public UUID getUuid() {
+ return UUID.fromString(this.uuid);
+ }
+ }
+
+}
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 1237a4af..9bdde6f9 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
@@ -23,9 +23,12 @@
*/
package com.lunarclient.apollo.example.nms;
+import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import lombok.Setter;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.Location;
@@ -38,6 +41,9 @@ public final class PlayerNpc {
private final Location location;
private final ServerPlayer handle;
+ @Setter
+ private List cosmetics = new ArrayList<>();
+
public int getEntityId() {
return this.handle.getId();
}
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java
index cdcbec81..116ffc4e 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CooldownProtoExample.java
@@ -44,7 +44,7 @@ public void displayCooldownItemExample(Player viewer) {
.setName("enderpearl-cooldown")
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15)))
.setIcon(Icon.newBuilder()
- .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_PEARL", 0, 0))
+ .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_PEARL", 0))
.build())
.build();
@@ -57,7 +57,7 @@ public void displayCooldownWithStyleExample(Player viewer) {
.setName("book-cooldown")
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(30)))
.setIcon(Icon.newBuilder()
- .setItemStack(ProtobufUtil.createItemStackIconProto("BOOK", 0, 0))
+ .setItemStack(ProtobufUtil.createItemStackIconProto("BOOK", 0))
.build())
.setStyle(CooldownStyle.newBuilder()
.setCircleStartColor(ProtobufUtil.createColorProto(new Color(255, 85, 85))) // ApolloColors.RED
@@ -77,7 +77,7 @@ public void displayCooldownWithPlayerTextureExample(Player viewer) {
.setDuration(ProtobufUtil.createDurationProto(Duration.ofSeconds(15)))
.setIcon(Icon.newBuilder()
.setItemStack(ProtobufUtil.createItemStackIconProto(
- "PLAYER_HEAD", 0, 0,
+ "PLAYER_HEAD", 0, null,
ProtobufUtil.createProfileProto(
UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"),
"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19",
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/WaypointProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/WaypointProtoExample.java
index af0ad9e7..21918b8c 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/WaypointProtoExample.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/WaypointProtoExample.java
@@ -29,6 +29,7 @@
import com.lunarclient.apollo.waypoint.v1.DisplayWaypointMessage;
import com.lunarclient.apollo.waypoint.v1.RemoveWaypointMessage;
import com.lunarclient.apollo.waypoint.v1.ResetWaypointsMessage;
+import com.lunarclient.apollo.waypoint.v1.WaypointTextStyle;
import java.awt.Color;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@@ -48,6 +49,24 @@ public void displayWaypointExample(Player viewer) {
ProtobufPacketUtil.sendPacket(viewer, message);
}
+ @Override
+ public void displayWaypointWithTextStyle(Player viewer) {
+ DisplayWaypointMessage message = DisplayWaypointMessage.newBuilder()
+ .setName("POI")
+ .setLocation(ProtobufUtil.createBlockLocationProto(new Location(viewer.getWorld(), -500, 100, -500)))
+ .setColor(ProtobufUtil.createColorProto(Color.GREEN))
+ .setPreventRemoval(false)
+ .setHidden(false)
+ .setStyle(WaypointTextStyle.newBuilder()
+ .setShowIcons(true)
+ .setLabelScale(0.5F)
+ .setTextIconScale(0.7F)
+ .build())
+ .build();
+
+ ProtobufPacketUtil.sendPacket(viewer, message);
+ }
+
@Override
public void removeWaypointExample(Player viewer) {
RemoveWaypointMessage message = RemoveWaypointMessage.newBuilder()
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java
index f719ecfc..97bea1c8 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java
@@ -52,6 +52,7 @@ public final class ProtobufPacketUtil {
static {
// Module Options the client needs to be notified about. These properties are sent with the enable module packet.
// While using the Apollo plugin this would be equivalent to modifying the config.yml
+ CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", Value.newBuilder().setBoolValue(false).build());
CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", Value.newBuilder().setBoolValue(false).build());
diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java
index 373fb9fa..40c2c31c 100644
--- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java
+++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufUtil.java
@@ -27,6 +27,7 @@
import com.lunarclient.apollo.common.v1.AdvancedResourceLocationIcon;
import com.lunarclient.apollo.common.v1.BlockLocation;
import com.lunarclient.apollo.common.v1.Cuboid2D;
+import com.lunarclient.apollo.common.v1.CustomModelData;
import com.lunarclient.apollo.common.v1.EntityId;
import com.lunarclient.apollo.common.v1.ItemStackIcon;
import com.lunarclient.apollo.common.v1.Profile;
@@ -35,6 +36,7 @@
import com.lunarclient.apollo.common.v1.Uuid;
import java.awt.Color;
import java.time.Duration;
+import java.util.List;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@@ -115,19 +117,22 @@ public static Location toBukkitLocation(com.lunarclient.apollo.common.v1.PlayerL
return location;
}
- public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData) {
- return ProtobufUtil.createItemStackIconProto(itemName, itemId, customModelData, null);
+ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId) {
+ return ProtobufUtil.createItemStackIconProto(itemName, itemId, null, null);
}
- public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, int customModelData, @Nullable Profile profile) {
+ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName, int itemId, @Nullable CustomModelData customModelData, @Nullable Profile profile) {
ItemStackIcon.Builder iconBuilder = ItemStackIcon.newBuilder()
- .setItemId(itemId)
- .setCustomModelData(customModelData);
+ .setItemId(itemId);
if (itemName != null) {
iconBuilder.setItemName(itemName);
}
+ if (customModelData != null) {
+ iconBuilder.setCustomModelDataObject(customModelData);
+ }
+
if (profile != null) {
iconBuilder.setProfile(profile);
}
@@ -135,6 +140,15 @@ public static ItemStackIcon createItemStackIconProto(@Nullable String itemName,
return iconBuilder.build();
}
+ public static CustomModelData createCustomModelDataProto(List floats, List flags, List strings, List colors) {
+ return CustomModelData.newBuilder()
+ .addAllFloats(floats)
+ .addAllFlags(flags)
+ .addAllStrings(strings)
+ .addAllColors(colors)
+ .build();
+ }
+
public static Profile createProfileProto(@Nullable UUID id, String texture, String signature) {
Profile.Builder builder = Profile.newBuilder()
.setTexture(texture)
diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml
index 442e5445..ed36333d 100644
--- a/example/bukkit/proto/src/main/resources/plugin.yml
+++ b/example/bukkit/proto/src/main/resources/plugin.yml
@@ -9,6 +9,7 @@ folia-supported: true
commands:
npc:
description: "NPCs!"
+ aliases: [apollonpc]
autotexthotkey:
description: "Auto Text Hotkey!"
beam:
diff --git a/gradle.properties b/gradle.properties
index 54bb2c7c..8668f6b6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
group=com.lunarclient
-version=1.2.6
+version=1.2.7-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 cb68d5c4..f2b33187 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.1.6"
+protobuf = "0.1.7"
gson = "2.10.1"
shadow = "9.4.1"
spotless = "8.4.0"
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 2825214a..f074ff32 100644
--- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java
+++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java
@@ -94,6 +94,7 @@
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.ChannelRegistrar;
+import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -118,6 +119,7 @@
public final class ApolloVelocityPlatform implements ApolloPlatform {
public static MinecraftChannelIdentifier PLUGIN_CHANNEL;
+ public static LegacyChannelIdentifier LEGACY_PLUGIN_CHANNEL;
@Getter private static ApolloVelocityPlatform instance;
@@ -227,6 +229,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) {
ChannelRegistrar channelRegistrar = this.server.getChannelRegistrar();
channelRegistrar.register(ApolloVelocityPlatform.PLUGIN_CHANNEL);
+ channelRegistrar.register(ApolloVelocityPlatform.LEGACY_PLUGIN_CHANNEL);
channelRegistrar.register(ApolloMetadataListener.FML_HANDSHAKE_CHANNEL);
CommandManager commandManager = this.server.getCommandManager();
@@ -250,6 +253,7 @@ public void onProxyShutdown(ProxyShutdownEvent event) {
static {
PLUGIN_CHANNEL = MinecraftChannelIdentifier.create("lunar", "apollo");
+ LEGACY_PLUGIN_CHANNEL = new LegacyChannelIdentifier(ApolloManager.PLUGIN_MESSAGE_CHANNEL);
}
}
diff --git a/platform/velocity/src/main/java/com/lunarclient/apollo/listener/ApolloPlayerListener.java b/platform/velocity/src/main/java/com/lunarclient/apollo/listener/ApolloPlayerListener.java
index d5a4906f..f7870a65 100644
--- a/platform/velocity/src/main/java/com/lunarclient/apollo/listener/ApolloPlayerListener.java
+++ b/platform/velocity/src/main/java/com/lunarclient/apollo/listener/ApolloPlayerListener.java
@@ -25,7 +25,6 @@
import com.lunarclient.apollo.Apollo;
import com.lunarclient.apollo.ApolloManager;
-import com.lunarclient.apollo.ApolloVelocityPlatform;
import com.lunarclient.apollo.player.ApolloPlayerManagerImpl;
import com.lunarclient.apollo.wrapper.VelocityApolloPlayer;
import com.velocitypowered.api.event.Subscribe;
@@ -33,6 +32,7 @@
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
/**
* Handles registration and un-registration of Apollo players.
@@ -49,7 +49,15 @@ public final class ApolloPlayerListener {
*/
@Subscribe
public void onPlayerRegisterChannel(PlayerChannelRegisterEvent event) {
- if (!event.getChannels().contains(ApolloVelocityPlatform.PLUGIN_CHANNEL)) {
+ boolean registered = false;
+ for (ChannelIdentifier channel : event.getChannels()) {
+ if (channel.getId().equals(ApolloManager.PLUGIN_MESSAGE_CHANNEL)) {
+ registered = true;
+ break;
+ }
+ }
+
+ if (!registered) {
return;
}
@@ -65,7 +73,7 @@ public void onPlayerRegisterChannel(PlayerChannelRegisterEvent event) {
*/
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
- if (!event.getIdentifier().equals(ApolloVelocityPlatform.PLUGIN_CHANNEL)) {
+ if (!event.getIdentifier().getId().equals(ApolloManager.PLUGIN_MESSAGE_CHANNEL)) {
return;
}