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/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/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java index d6ff7b8f..c2c03685 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/util/JsonPacketUtil.java @@ -51,6 +51,7 @@ public final class JsonPacketUtil { static { // Module Options the client needs to be notified about. These properties are sent with the enable module packet. // While using the Apollo plugin this would be equivalent to modifying the config.yml + CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", false); CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", false); diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index 027ac7db..ba45de5e 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -9,6 +9,7 @@ folia-supported: true commands: npc: description: "NPCs!" + aliases: [apollonpc] autotexthotkey: description: "Auto Text Hotkey!" beam: diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java new file mode 100644 index 00000000..1fed7076 --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CommandCosmetic.java @@ -0,0 +1,67 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class CommandCosmetic { + + int id; + Options options; + + public abstract static class Options { + } + + @Getter + @Builder + public static final class Hat extends Options { + @Builder.Default boolean showOverHelmet = true; + @Builder.Default boolean showOverSkinLayer = true; + @Builder.Default float heightOffset = 0f; + } + + @Getter + @Builder + public static final class Cloak extends Options { + @Builder.Default boolean useClothPhysics = false; + } + + @Getter + @Builder + public static final class Pet extends Options { + @Builder.Default boolean flipShoulder = false; + } + + @Getter + @Builder + public static final class Body extends Options { + @Builder.Default boolean showOverChestplate = true; + @Builder.Default boolean showOverLeggings = true; + @Builder.Default boolean showOverBoots = true; + } + +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java new file mode 100644 index 00000000..8aa99497 --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/CosmeticOptionsAdapter.java @@ -0,0 +1,85 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; + +public final class CosmeticOptionsAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(CommandCosmetic.Options options, Type cosmeticType, JsonSerializationContext context) { + if (options == null) { + return JsonNull.INSTANCE; + } + + JsonObject object; + String type; + if (options instanceof CommandCosmetic.Hat) { + object = context.serialize(options, CommandCosmetic.Hat.class).getAsJsonObject(); + type = "hat"; + } else if (options instanceof CommandCosmetic.Cloak) { + object = context.serialize(options, CommandCosmetic.Cloak.class).getAsJsonObject(); + type = "cloak"; + } else if (options instanceof CommandCosmetic.Pet) { + object = context.serialize(options, CommandCosmetic.Pet.class).getAsJsonObject(); + type = "pet"; + } else if (options instanceof CommandCosmetic.Body) { + object = context.serialize(options, CommandCosmetic.Body.class).getAsJsonObject(); + type = "body"; + } else { + return JsonNull.INSTANCE; + } + + object.addProperty("type", type); + return object; + } + + @Override + public CommandCosmetic.Options deserialize(JsonElement json, Type cosmeticType, JsonDeserializationContext context) { + if (json == null || json.isJsonNull() || !json.isJsonObject()) { + return null; + } + + JsonObject object = json.getAsJsonObject(); + JsonElement element = object.get("type"); + if (element == null || element.isJsonNull()) { + return null; + } + + switch (element.getAsString()) { + case "hat": return context.deserialize(object, CommandCosmetic.Hat.class); + case "cloak": return context.deserialize(object, CommandCosmetic.Cloak.class); + case "pet": return context.deserialize(object, CommandCosmetic.Pet.class); + case "body": return context.deserialize(object, CommandCosmetic.Body.class); + default: return null; + } + } +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java index 22f1d0b2..be00b996 100644 --- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java @@ -67,12 +67,14 @@ public final class NpcManager implements Listener { private final Map npcs = new HashMap<>(); private final JavaPlugin plugin; + private final NpcStore store; public NpcManager(JavaPlugin plugin) { this.plugin = plugin; + this.store = new NpcStore(plugin); Bukkit.getPluginManager().registerEvents(this, plugin); - Bukkit.getScheduler().runTask(plugin, this::spawnDefaultNpcs); + Bukkit.getScheduler().runTask(plugin, this::loadOrSpawnDefaults); } public void removeNpc(UUID uuid) { @@ -82,9 +84,10 @@ public void removeNpc(UUID uuid) { } this.despawnNpcs(npc); + this.store.save(this.npcs.values()); } - public void removeAll() { + public void despawnAll() { for (PlayerNpc npc : new ArrayList<>(this.npcs.values())) { this.despawnNpcs(npc); } @@ -98,6 +101,14 @@ public Optional findByName(String name) { .findFirst(); } + public Optional findByUuid(UUID uuid) { + return Optional.ofNullable(this.npcs.get(uuid)); + } + + public void save() { + this.store.save(this.npcs.values()); + } + public Collection getNpcs() { return new ArrayList<>(this.npcs.values()); } @@ -118,11 +129,43 @@ public void onPlayerQuit(PlayerQuitEvent event) { } } + private void loadOrSpawnDefaults() { + if (!this.store.exists()) { + this.spawnDefaultNpcs(); + this.store.save(this.npcs.values()); + return; + } + + for (NpcStore.Entry entry : this.store.load()) { + Location location = this.store.toLocation(entry); + + if (location == null) { + continue; + } + + PlayerNpc npc = this.spawnNpc(entry.getName(), location, entry.getUuid()); + if (npc != null) { + npc.setCosmetics(new ArrayList<>(entry.getCosmetics())); + } + } + } + private void spawnDefaultNpcs() { this.spawnNpc("Apollo", new Location(Bukkit.getWorld("world"), 20.5, 65, 5.5, 90f, 0f)); } public @Nullable PlayerNpc spawnNpc(String name, Location location) { + UUID npcUuid = new UUID(UUID.randomUUID().getMostSignificantBits(), 0L); + PlayerNpc npc = this.spawnNpc(name, location, npcUuid); + + if (npc != null) { + this.store.save(this.npcs.values()); + } + + return npc; + } + + public @Nullable PlayerNpc spawnNpc(String name, Location location, UUID npcUuid) { World world = location.getWorld(); if (world == null) { return null; @@ -131,7 +174,6 @@ private void spawnDefaultNpcs() { MinecraftServer server = MinecraftServer.getServer(); ServerLevel level = ((CraftWorld) world).getHandle(); - UUID npcUuid = new UUID(UUID.randomUUID().getMostSignificantBits(), 0L); GameProfile profile = new GameProfile(npcUuid, name); ServerPlayer npc = new ServerPlayer(server, level, profile, NpcManager.NPC_CLIENT_INFO); diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java new file mode 100644 index 00000000..36635a95 --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcStore.java @@ -0,0 +1,150 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.plugin.java.JavaPlugin; + +public final class NpcStore { + + private static final String FILE_NAME = "npcs.json"; + private static final Type ENTRY_LIST_TYPE = new TypeToken>() { }.getType(); + + private final JavaPlugin plugin; + private final Gson gson; + private final File file; + + public NpcStore(JavaPlugin plugin) { + this.plugin = plugin; + + this.gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(CommandCosmetic.Options.class, new CosmeticOptionsAdapter()) + .create(); + + this.file = new File(plugin.getDataFolder(), NpcStore.FILE_NAME); + + File dataFolder = plugin.getDataFolder(); + if (!dataFolder.exists() && !dataFolder.mkdirs()) { + plugin.getLogger().warning("Failed to create plugin data folder at " + dataFolder.getAbsolutePath()); + } + } + + public boolean exists() { + return this.file.isFile(); + } + + public List load() { + if (!this.file.isFile()) { + return new ArrayList<>(); + } + + try (Reader reader = Files.newBufferedReader(this.file.toPath(), StandardCharsets.UTF_8)) { + List entries = this.gson.fromJson(reader, NpcStore.ENTRY_LIST_TYPE); + return entries != null ? entries : new ArrayList<>(); + } catch (IOException e) { + this.plugin.getLogger().warning("Failed to read " + NpcStore.FILE_NAME + ": " + e.getMessage()); + return new ArrayList<>(); + } + } + + public void save(Collection npcs) { + List entries = new ArrayList<>(npcs.size()); + for (PlayerNpc npc : npcs) { + Location location = npc.getLocation(); + World world = location.getWorld(); + if (world == null) { + continue; + } + + entries.add(new Entry( + npc.getUuid().toString(), npc.getName(), world.getName(), + location.getX(), location.getY(), location.getZ(), + location.getYaw(), location.getPitch(), + new ArrayList<>(npc.getCosmetics()) + )); + } + + try { + Files.createDirectories(this.file.toPath().getParent()); + } catch (IOException e) { + this.plugin.getLogger().warning("Failed to create plugin data folder for " + NpcStore.FILE_NAME + ": " + e.getMessage()); + return; + } + + try (Writer writer = Files.newBufferedWriter(this.file.toPath(), StandardCharsets.UTF_8)) { + this.gson.toJson(entries, NpcStore.ENTRY_LIST_TYPE, writer); + } catch (IOException e) { + this.plugin.getLogger().warning("Failed to write " + NpcStore.FILE_NAME + ": " + e.getMessage()); + } + } + + public Location toLocation(Entry entry) { + World world = Bukkit.getWorld(entry.world); + + if (world == null) { + return null; + } + + return new Location(world, entry.x, entry.y, entry.z, entry.yaw, entry.pitch); + } + + @Getter + @RequiredArgsConstructor + public static final class Entry { + + private final String uuid; + private final String name; + private final String world; + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + private final List cosmetics; + + public UUID getUuid() { + return UUID.fromString(this.uuid); + } + } + +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java index 1237a4af..9bdde6f9 100644 --- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java @@ -23,9 +23,12 @@ */ package com.lunarclient.apollo.example.nms; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import net.minecraft.server.level.ServerPlayer; import org.bukkit.Location; @@ -38,6 +41,9 @@ public final class PlayerNpc { private final Location location; private final ServerPlayer handle; + @Setter + private List cosmetics = new ArrayList<>(); + public int getEntityId() { return this.handle.getId(); } diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java index f719ecfc..97bea1c8 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/util/ProtobufPacketUtil.java @@ -52,6 +52,7 @@ public final class ProtobufPacketUtil { static { // Module Options the client needs to be notified about. These properties are sent with the enable module packet. // While using the Apollo plugin this would be equivalent to modifying the config.yml + CONFIG_MODULE_PROPERTIES.put("colored_fire", "persist-colors-on-unload", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("combat", "disable-miss-penalty", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", Value.newBuilder().setBoolValue(false).build()); diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index 442e5445..ed36333d 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -9,6 +9,7 @@ folia-supported: true commands: npc: description: "NPCs!" + aliases: [apollonpc] autotexthotkey: description: "Auto Text Hotkey!" beam: diff --git a/gradle.properties b/gradle.properties index 54bb2c7c..8668f6b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.lunarclient -version=1.2.6 +version=1.2.7-SNAPSHOT description=The API for interacting with Lunar Client players. org.gradle.parallel=true