From 46cb2d781bb15b5a6e1b91df6a8b05254e68aa54 Mon Sep 17 00:00:00 2001 From: ItsNature Date: Wed, 13 May 2026 11:21:00 +0200 Subject: [PATCH 1/6] Deploy as `1.2.7-SNAPSHOT` --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 06a5855552d0895b5acab45dcff6c927e05ec52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Thu, 14 May 2026 15:26:26 +0200 Subject: [PATCH 2/6] example(internal): persistent npcs (#280) --- .../api/listener/ApolloPlayerApiListener.java | 9 +- .../api/module/CosmeticApiExample.java | 52 ++++++ .../bukkit/api/src/main/resources/plugin.yml | 1 + .../apollo/example/ApolloExamplePlugin.java | 2 +- .../example/command/CosmeticCommand.java | 153 ++++++++++++++++++ .../apollo/example/command/NpcCommand.java | 12 -- .../example/module/impl/CosmeticExample.java | 10 ++ .../apollo/example/util/CommandUtil.java | 47 ++++++ .../bukkit/json/src/main/resources/plugin.yml | 1 + .../apollo/example/nms/CommandCosmetic.java | 67 ++++++++ .../example/nms/CosmeticOptionsAdapter.java | 85 ++++++++++ .../apollo/example/nms/NpcManager.java | 48 +++++- .../apollo/example/nms/NpcStore.java | 150 +++++++++++++++++ .../apollo/example/nms/PlayerNpc.java | 6 + .../proto/src/main/resources/plugin.yml | 1 + 15 files changed, 625 insertions(+), 19 deletions(-) create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/CommandUtil.java create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.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..58b7d4ed 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 { @@ -79,8 +79,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/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/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/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/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/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: From 908271d30409f511e7880595b9e7e1a82f3f1e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 19 May 2026 17:10:47 +0200 Subject: [PATCH 3/6] Add `PERSIST_COLORS_ON_UNLOAD` to ColoredFireModule (#281) --- .../module/coloredfire/ColoredFireModule.java | 19 +++++++++++++++++++ .../lightweight/json/packet-util.mdx | 1 + .../lightweight/protobuf/packet-util.mdx | 1 + .../example/json/util/JsonPacketUtil.java | 1 + .../proto/util/ProtobufPacketUtil.java | 1 + 5 files changed, 23 insertions(+) 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/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/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/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/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()); From 75aefdb15cda9f13c06c7d3eb3612a3190da070c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 25 May 2026 12:36:12 +0200 Subject: [PATCH 4/6] Add modern Custom Model Data (#284) --- .../apollo/common/icon/CustomModelData.java | 79 +++++++++++++++++++ .../apollo/common/icon/ItemStackIcon.java | 10 +++ .../apollo/network/NetworkTypes.java | 44 +++++++++++ .../lightweight/json/object-util.mdx | 32 +++++++- docs/developers/lightweight/protobuf.mdx | 6 +- .../lightweight/protobuf/object-util.mdx | 22 ++++-- docs/developers/modules/cooldown.mdx | 12 +-- docs/developers/utilities/icons.mdx | 50 +++++++++++- .../json/module/CooldownJsonExample.java | 6 +- .../apollo/example/json/util/JsonUtil.java | 34 +++++++- .../proto/module/CooldownProtoExample.java | 6 +- .../example/proto/util/ProtobufUtil.java | 24 ++++-- gradle/libs.versions.toml | 2 +- 13 files changed, 289 insertions(+), 38 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/common/icon/CustomModelData.java 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/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/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/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/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/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/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/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/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/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" From 334221db33b94e0e529d13033e00610e1d87c112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 25 May 2026 16:48:10 +0200 Subject: [PATCH 5/6] fix(velocity): legacy player detection (#285) --- .../lunarclient/apollo/ApolloVelocityPlatform.java | 4 ++++ .../apollo/listener/ApolloPlayerListener.java | 14 +++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) 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; } From f44fb17b8eee5160711033c0c14f76b0b15e68a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Tue, 26 May 2026 00:17:56 +0200 Subject: [PATCH 6/6] Various additions to the Waypoint Module (#283) --- .../apollo/module/waypoint/Waypoint.java | 42 +++++ .../module/waypoint/WaypointModule.java | 18 ++ .../module/waypoint/WaypointTextStyle.java | 116 ++++++++++++ .../module/waypoint/WaypointModuleImpl.java | 141 ++++++++++++++- docs/developers/modules/waypoint.mdx | 171 ++++++++++++++++++ .../api/listener/ApolloPlayerApiListener.java | 1 - .../api/module/WaypointApiExample.java | 28 +++ .../example/command/WaypointCommand.java | 10 +- .../example/module/impl/WaypointExample.java | 2 + .../json/module/WaypointJsonExample.java | 21 +++ .../proto/module/WaypointProtoExample.java | 19 ++ 11 files changed, 558 insertions(+), 11 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointTextStyle.java 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/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. ![Waypoint Module Example](/modules/waypoint/overview.png#center) @@ -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/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 58b7d4ed..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 @@ -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); 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/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/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/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/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()