From 08b6bed2fb0b87524be27fc6b7965fcc317e1254 Mon Sep 17 00:00:00 2001 From: ItsNature Date: Thu, 2 Jul 2026 01:58:25 +0200 Subject: [PATCH 1/9] Deploy as `1.2.9-SNAPSHOT` --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1cecc874..43cc0bbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.lunarclient -version=1.2.8 +version=1.2.9-SNAPSHOT description=The API for interacting with Lunar Client players. org.gradle.parallel=true From 9d25ce46f434d97ebb1704c0ba1f4dbe2140c489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 10 Aug 2026 23:01:10 +0200 Subject: [PATCH 2/9] Feature - Height Limit Module (#300) * Height Limit Module * Add default height limit config entry, more callouts & update example --- .../module/heightlimit/HeightLimit.java | 70 ++++++ .../module/heightlimit/HeightLimitModule.java | 118 ++++++++++ .../heightlimit/HeightLimitModuleImpl.java | 156 ++++++++++++++ .../PacketEnrichmentImpl.java | 4 + .../module/waypoint/WaypointModuleImpl.java | 4 + docs/developers/lightweight/protobuf.mdx | 6 +- docs/developers/modules.mdx | 2 + docs/developers/modules/_meta.json | 1 + docs/developers/modules/heightlimit.mdx | 202 ++++++++++++++++++ docs/developers/modules/waypoint.mdx | 13 +- .../example/api/ApolloApiExamplePlatform.java | 2 + .../api/module/HeightLimitApiExample.java | 66 ++++++ .../bukkit/api/src/main/resources/plugin.yml | 2 + .../apollo/example/ApolloExamplePlugin.java | 4 + .../example/command/HeightLimitCommand.java | 79 +++++++ .../module/impl/HeightLimitExample.java | 37 ++++ .../json/ApolloJsonExamplePlatform.java | 2 + .../json/module/HeightLimitJsonExample.java | 66 ++++++ .../bukkit/json/src/main/resources/plugin.yml | 2 + .../proto/ApolloProtoExamplePlatform.java | 2 + .../proto/module/HeightLimitProtoExample.java | 66 ++++++ .../proto/src/main/resources/plugin.yml | 2 + .../apollo/common/ApolloComponent.java | 24 +++ gradle/libs.versions.toml | 2 +- .../apollo/ApolloBukkitPlatform.java | 3 + .../apollo/ApolloBungeePlatform.java | 3 + .../apollo/ApolloFoliaPlatform.java | 3 + .../apollo/ApolloMinestomPlatform.java | 3 + .../apollo/ApolloVelocityPlatform.java | 3 + 29 files changed, 942 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java create mode 100644 docs/developers/modules/heightlimit.mdx create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java diff --git a/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java new file mode 100644 index 00000000..78035f57 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimit.java @@ -0,0 +1,70 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.heightlimit; + +import lombok.Builder; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +/** + * Represents a height limit which can be shown on the client. + * + * @since 1.2.9 + */ +@Getter +@Builder +public final class HeightLimit { + + /** + * Returns the height limit {@link String} world name. + * + * @return the height limit world name + * @since 1.2.9 + */ + String world; + + /** + * Returns the height limit {@link Integer} Y level where block placement + * is denied. + * + *

The highest buildable layer is {@code limit - 1}.

+ * + * @return the height limit + * @since 1.2.9 + */ + @Range(from = 1, to = Integer.MAX_VALUE) int limit; + + /** + * Returns the height limit {@link Component} display name. + * + *

Shown on the client's height limit HUD.

+ * + * @return the height limit display name + * @since 1.2.9 + */ + @Nullable Component displayName; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java new file mode 100644 index 00000000..86431321 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModule.java @@ -0,0 +1,118 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.heightlimit; + +import com.lunarclient.apollo.module.ApolloModule; +import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.option.ListOption; +import com.lunarclient.apollo.option.Option; +import com.lunarclient.apollo.recipients.Recipients; +import io.leangen.geantyref.TypeToken; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the height limit module. + * + *

Sets the build-height limit shown by the clients Height Limit mod + * (block overlay and HUD display). The client resolves the active limit + * against the world the player is currently in.

+ * + *

This module only provides a visual indicator for the player. The + * server is still responsible for cancelling block placement above the + * height limit.

+ * + * @since 1.2.9 + */ +@ApiStatus.NonExtendable +@ModuleDefinition(id = "height_limit", name = "Height Limit") +public abstract class HeightLimitModule extends ApolloModule { + + private static final HeightLimit OVERWORLD_HEIGHT_LIMIT = HeightLimit.builder() + .world("world") + .limit(200) + .displayName(Component.text("Overworld", NamedTextColor.GOLD)) + .build(); + + /** + * Returns the default list of height limits to send to the player. + * + * @since 1.2.9 + */ + public static final ListOption DEFAULT_HEIGHT_LIMITS = Option.list() + .comment("Sets the default height limits to send to the player.") + .node("default-height-limits").type(new TypeToken>() {}) + .defaultValue(new ArrayList<>(Collections.singletonList(HeightLimitModule.OVERWORLD_HEIGHT_LIMIT))) + .build(); + + protected HeightLimitModule() { + this.registerOptions( + ApolloModule.ENABLE_OPTION_OFF, + HeightLimitModule.DEFAULT_HEIGHT_LIMITS + ); + } + + /** + * Overrides the {@link HeightLimit} for the {@link Recipients}. + * + *

Sending a height limit for an already-known world replaces + * that worlds entry.

+ * + * @param recipients the recipients that are receiving the packet + * @param heightLimit the height limit + * @since 1.2.9 + */ + public abstract void overrideHeightLimit(Recipients recipients, HeightLimit heightLimit); + + /** + * Removes the {@link HeightLimit} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param world the world name + * @since 1.2.9 + */ + public abstract void removeHeightLimit(Recipients recipients, String world); + + /** + * Removes the {@link HeightLimit} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param heightLimit the height limit + * @since 1.2.9 + */ + public abstract void removeHeightLimit(Recipients recipients, HeightLimit heightLimit); + + /** + * Resets all {@link HeightLimit}s for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.9 + */ + public abstract void resetHeightLimits(Recipients recipients); + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java new file mode 100644 index 00000000..e8887aed --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/heightlimit/HeightLimitModuleImpl.java @@ -0,0 +1,156 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.heightlimit; + +import com.lunarclient.apollo.ApolloManager; +import com.lunarclient.apollo.common.ApolloComponent; +import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent; +import com.lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage; +import com.lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage; +import com.lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage; +import com.lunarclient.apollo.option.config.Serializer; +import com.lunarclient.apollo.player.ApolloPlayer; +import com.lunarclient.apollo.recipients.Recipients; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import lombok.NonNull; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import static com.lunarclient.apollo.util.Ranges.checkStrictlyPositive; + +/** + * Provides the height limit module. + * + * @since 1.2.9 + */ +public final class HeightLimitModuleImpl extends HeightLimitModule implements Serializer { + + /** + * Creates a new instance of {@link HeightLimitModuleImpl}. + * + * @since 1.2.9 + */ + public HeightLimitModuleImpl() { + super(); + this.serializer(HeightLimit.class, new HeightLimitSerializer()); + this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister); + } + + @Override + public void overrideHeightLimit(@NonNull Recipients recipients, @NonNull HeightLimit heightLimit) { + OverrideHeightLimitMessage.Builder builder = OverrideHeightLimitMessage.newBuilder() + .setWorld(heightLimit.getWorld()) + .setLimit(checkStrictlyPositive(heightLimit.getLimit(), "HeightLimit#limit")); + + Component displayName = heightLimit.getDisplayName(); + if (displayName != null) { + builder.setDisplayNameAdventureJsonLines(ApolloComponent.toJson(displayName)); + } + + OverrideHeightLimitMessage message = builder.build(); + ApolloManager.getNetworkManager().sendPacket(recipients, message); + } + + @Override + public void removeHeightLimit(@NonNull Recipients recipients, @NonNull String world) { + RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder() + .setWorld(world) + .build(); + + ApolloManager.getNetworkManager().sendPacket(recipients, message); + } + + @Override + public void removeHeightLimit(@NonNull Recipients recipients, @NonNull HeightLimit heightLimit) { + this.removeHeightLimit(recipients, heightLimit.getWorld()); + } + + @Override + public void resetHeightLimits(@NonNull Recipients recipients) { + ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance(); + ApolloManager.getNetworkManager().sendPacket(recipients, message); + } + + private void onPlayerRegister(ApolloRegisterPlayerEvent event) { + if (!this.isEnabled()) { + return; + } + + ApolloPlayer player = event.getPlayer(); + List heightLimits = this.getOptions().get(player, HeightLimitModule.DEFAULT_HEIGHT_LIMITS); + + if (heightLimits != null) { + for (HeightLimit heightLimit : heightLimits) { + this.overrideHeightLimit(player, heightLimit); + } + } + } + + private static final class HeightLimitSerializer implements TypeSerializer { + + @Override + public HeightLimit deserialize(Type type, ConfigurationNode node) throws SerializationException { + HeightLimit.HeightLimitBuilder builder = HeightLimit.builder() + .world(this.virtualNode(node, "world").getString()) + .limit(this.virtualNode(node, "limit").getInt()); + + String displayName = node.node("display-name").getString(); + if (displayName != null) { + builder.displayName(ApolloComponent.fromLegacyAmpersand(displayName)); + } + + return builder.build(); + } + + @Override + public void serialize(Type type, @Nullable HeightLimit heightLimit, ConfigurationNode node) throws SerializationException { + if (heightLimit == null) { + node.raw(null); + return; + } + + node.node("world").set(heightLimit.getWorld()); + node.node("limit").set(heightLimit.getLimit()); + + Component displayName = heightLimit.getDisplayName(); + if (displayName != null) { + node.node("display-name").set(ApolloComponent.toLegacyAmpersand(displayName)); + } + } + + private ConfigurationNode virtualNode(ConfigurationNode source, Object... path) throws SerializationException { + if (!source.hasChild(path)) { + throw new SerializationException("Required field " + Arrays.toString(path) + " not found!"); + } + + return source.node(path); + } + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java index 1446ef11..cbc97f0d 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java @@ -56,6 +56,10 @@ public PacketEnrichmentImpl() { } private void onReceivePacket(ApolloReceivePacketEvent event) { + if (!this.isEnabled()) { + return; + } + Options options = this.getOptions(); if (options.get(PacketEnrichmentModule.PLAYER_ATTACK_EVENT)) { diff --git a/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java index b9ed8e3b..320eab67 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/waypoint/WaypointModuleImpl.java @@ -110,6 +110,10 @@ public void hideWaypoint(@NonNull Recipients recipients, @NonNull String waypoin } private void onPlayerRegister(ApolloRegisterPlayerEvent event) { + if (!this.isEnabled()) { + return; + } + ApolloPlayer player = event.getPlayer(); List waypoints = this.getOptions().get(player, WaypointModule.DEFAULT_WAYPOINTS); diff --git a/docs/developers/lightweight/protobuf.mdx b/docs/developers/lightweight/protobuf.mdx index f39b9e45..2759440a 100644 --- a/docs/developers/lightweight/protobuf.mdx +++ b/docs/developers/lightweight/protobuf.mdx @@ -26,7 +26,7 @@ Available fields for each message, including their types, are available on the B com.lunarclient apollo-protos - 0.2.0 + 0.2.1 ``` @@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api 'com.lunarclient:apollo-protos:0.2.0' + api 'com.lunarclient:apollo-protos:0.2.1' } ``` @@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api("com.lunarclient:apollo-protos:0.2.0") + api("com.lunarclient:apollo-protos:0.2.1") } ``` diff --git a/docs/developers/modules.mdx b/docs/developers/modules.mdx index 16803d54..1d8f8fc7 100644 --- a/docs/developers/modules.mdx +++ b/docs/developers/modules.mdx @@ -16,9 +16,11 @@ These modules are available to all servers using Apollo and do not require any p πŸ”— [Entity](/apollo/developers/modules/entity)
πŸ”— [Glint](/apollo/developers/modules/glint)
πŸ”— [Glow](/apollo/developers/modules/glow)
+πŸ”— [Height Limit](/apollo/developers/modules/heightlimit)
πŸ”— [Hologram](/apollo/developers/modules/hologram)
πŸ”— [Inventory](/apollo/developers/modules/inventory)
πŸ”— [Limb](/apollo/developers/modules/limb)
+πŸ”— [Marker](/apollo/developers/modules/marker)
πŸ”— [Mod Setting](/apollo/developers/modules/modsetting)
πŸ”— [Nametag](/apollo/developers/modules/nametag)
πŸ”— [Nick Hider](/apollo/developers/modules/nickhider)
diff --git a/docs/developers/modules/_meta.json b/docs/developers/modules/_meta.json index 182a8a35..11e872d0 100644 --- a/docs/developers/modules/_meta.json +++ b/docs/developers/modules/_meta.json @@ -9,6 +9,7 @@ "entity": "Entity", "glint": "Glint", "glow": "Glow", + "heightlimit": "Height Limit", "hologram": "Hologram", "inventory": "Inventory", "limb": "Limb", diff --git a/docs/developers/modules/heightlimit.mdx b/docs/developers/modules/heightlimit.mdx new file mode 100644 index 00000000..b22148dd --- /dev/null +++ b/docs/developers/modules/heightlimit.mdx @@ -0,0 +1,202 @@ +import { Callout, Tab, Tabs } from 'nextra-theme-docs' + +# Height Limit Module + +## Overview + +The height limit module allows servers to control the build-height limit displayed by the Height Limit mod. + +- Adds the ability to set a height limit per world, rendered as a block overlay and HUD display. + + + This module is disabled by default, if you wish to use this module you will need to enable it in `config.yml`. + + + + This module only provides a visual indicator for the player, the server is still responsible for cancelling block placement above the height limit. + + +## Integration + +### Sample Code +Explore each integration by cycling through each tab, to find the best fit for your requirements and needs. + + + + + + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + +### Overriding a Height Limit + +```java +public void overrideHeightLimitExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.heightLimitModule.overrideHeightLimit(apolloPlayer, HeightLimit.builder() + .world("world_the_end") + .limit(150) + .displayName(Component.text("The End", NamedTextColor.DARK_PURPLE)) + .build() + ); + }); +} +``` + +### Removing a Height Limit + +```java +public void removeHeightLimitExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.heightLimitModule.removeHeightLimit(apolloPlayer, "world_the_end")); +} +``` + +### Resetting all Height Limits + +```java +public void resetHeightLimitsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.heightLimitModule::resetHeightLimits); +} +``` + +### `HeightLimit` Options + +`.world(String)` is the world, by name, that you wish to apply the height limit to. Sending a height limit for an already-known world replaces that worlds entry. + +```java +.world("world_the_end") +``` + +`.limit(Integer)` is the Y level where block placement is denied. The highest buildable layer is `limit - 1`. + +```java +.limit(150) +``` + +`.displayName(Component)` is the optional display name shown on the clients height limit HUD. + +```java +.displayName(Component.text("The End", NamedTextColor.DARK_PURPLE)) +``` + + + + + + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + + + Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/protobuf/player-detection) example. + + +**Overriding a Height Limit** + +```java +public void overrideHeightLimitExample(Player viewer) { + OverrideHeightLimitMessage message = OverrideHeightLimitMessage.newBuilder() + .setWorld("world_the_end") + .setLimit(150) + .setDisplayNameAdventureJsonLines(AdventureUtil.toJson( + Component.text("The End", NamedTextColor.DARK_PURPLE) + )) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Removing a Height Limit** + +```java +public void removeHeightLimitExample(Player viewer) { + RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder() + .setWorld("world_the_end") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Height Limits** + +```java +public void resetHeightLimitsExample(Player viewer) { + ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + + + + + + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + + + Make sure the server is sending the world name to the client as show in the [Player Detection](/apollo/developers/lightweight/json/player-detection) example. + + +**Overriding a Height Limit** + +```java +public void overrideHeightLimitExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage"); + message.addProperty("world", "world_the_end"); + message.addProperty("limit", 150); + message.addProperty("display_name_adventure_json_lines", AdventureUtil.toJson( + Component.text("The End", NamedTextColor.DARK_PURPLE) + )); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Removing a Height Limit** + +```java +public void removeHeightLimitExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage"); + message.addProperty("world", "world_the_end"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Height Limits** + +```java +public void resetHeightLimitsExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + + + + + +## Available options + +- __`DEFAULT_HEIGHT_LIMITS`__ + - Sets the default height limits to send to the player. + - Values + - Type: `List` + - Default: + ```yaml + - world: world + limit: 200 + display-name: '&6Overworld' + ``` diff --git a/docs/developers/modules/waypoint.mdx b/docs/developers/modules/waypoint.mdx index d693cd68..78642822 100644 --- a/docs/developers/modules/waypoint.mdx +++ b/docs/developers/modules/waypoint.mdx @@ -433,7 +433,18 @@ public void resetWaypointsExample(Player viewer) { - Sets the default waypoints to send to the player. - Values - Type: `List` - - Default: `Empty List` + - Default: + ```yaml + - name: Spawn + location: + world: world + x: 0 + y: 100 + z: 0 + color: '#FF0000' + prevent-removal: false + hidden: false + ``` ## Automatic Waypoint Creation from Chat diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java index 2ae26a66..ffdc2396 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java @@ -40,6 +40,7 @@ import com.lunarclient.apollo.example.api.module.CosmeticApiExample; import com.lunarclient.apollo.example.api.module.EntityApiExample; import com.lunarclient.apollo.example.api.module.GlowApiExample; +import com.lunarclient.apollo.example.api.module.HeightLimitApiExample; import com.lunarclient.apollo.example.api.module.HologramApiExample; import com.lunarclient.apollo.example.api.module.LimbApiExample; import com.lunarclient.apollo.example.api.module.MarkerApiExample; @@ -93,6 +94,7 @@ public void registerModuleExamples() { this.setCooldownExample(new CooldownApiExample()); this.setEntityExample(new EntityApiExample()); this.setGlowExample(new GlowApiExample()); + this.setHeightLimitExample(new HeightLimitApiExample()); this.setHologramExample(new HologramApiExample()); this.setLimbExample(new LimbApiExample()); this.setMarkerExample(new MarkerApiExample()); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java new file mode 100644 index 00000000..39955281 --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/HeightLimitApiExample.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.example.module.impl.HeightLimitExample; +import com.lunarclient.apollo.module.heightlimit.HeightLimit; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class HeightLimitApiExample extends HeightLimitExample { + + private final HeightLimitModule heightLimitModule = Apollo.getModuleManager().getModule(HeightLimitModule.class); + + @Override + public void overrideHeightLimitExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.heightLimitModule.overrideHeightLimit(apolloPlayer, HeightLimit.builder() + .world("world_the_end") + .limit(150) + .displayName(Component.text("The End", NamedTextColor.DARK_PURPLE)) + .build() + ); + }); + } + + @Override + public void removeHeightLimitExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.heightLimitModule.removeHeightLimit(apolloPlayer, "world_the_end")); + } + + @Override + public void resetHeightLimitsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.heightLimitModule::resetHeightLimits); + } + +} diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml index 9ba142c0..c16d024b 100644 --- a/example/bukkit/api/src/main/resources/plugin.yml +++ b/example/bukkit/api/src/main/resources/plugin.yml @@ -36,6 +36,8 @@ commands: description: "Glint!" glow: description: "Glow!" + heightlimit: + description: "Height Limit!" hologram: description: "Holograms!" inventory: diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java index c39d80f2..94092492 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java @@ -34,6 +34,7 @@ import com.lunarclient.apollo.example.command.EntityCommand; import com.lunarclient.apollo.example.command.GlintCommand; import com.lunarclient.apollo.example.command.GlowCommand; +import com.lunarclient.apollo.example.command.HeightLimitCommand; import com.lunarclient.apollo.example.command.HologramCommand; import com.lunarclient.apollo.example.command.InventoryCommand; import com.lunarclient.apollo.example.command.LimbCommand; @@ -68,6 +69,7 @@ import com.lunarclient.apollo.example.module.impl.EntityExample; import com.lunarclient.apollo.example.module.impl.GlintExample; import com.lunarclient.apollo.example.module.impl.GlowExample; +import com.lunarclient.apollo.example.module.impl.HeightLimitExample; import com.lunarclient.apollo.example.module.impl.HologramExample; import com.lunarclient.apollo.example.module.impl.InventoryExample; import com.lunarclient.apollo.example.module.impl.LimbExample; @@ -114,6 +116,7 @@ public abstract class ApolloExamplePlugin extends JavaPlugin { private EntityExample entityExample; private GlintExample glintExample; private GlowExample glowExample; + private HeightLimitExample heightLimitExample; private HologramExample hologramExample; private InventoryExample inventoryExample; private LimbExample limbExample; @@ -173,6 +176,7 @@ private void registerCommonCommands() { this.getCommand("entity").setExecutor(new EntityCommand()); this.getCommand("glint").setExecutor(new GlintCommand()); this.getCommand("glow").setExecutor(new GlowCommand()); + this.getCommand("heightlimit").setExecutor(new HeightLimitCommand()); this.getCommand("hologram").setExecutor(new HologramCommand()); this.getCommand("inventory").setExecutor(new InventoryCommand()); this.getCommand("limb").setExecutor(new LimbCommand()); diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java new file mode 100644 index 00000000..7a4a3e68 --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/HeightLimitCommand.java @@ -0,0 +1,79 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.command; + +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.module.impl.HeightLimitExample; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class HeightLimitCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("Player only!"); + return true; + } + + Player player = (Player) sender; + + if (args.length != 1) { + player.sendMessage("Usage: /heightlimit "); + return true; + } + + HeightLimitExample heightLimitExample = ApolloExamplePlugin.getInstance().getHeightLimitExample(); + + switch (args[0].toLowerCase()) { + case "override": { + heightLimitExample.overrideHeightLimitExample(player); + player.sendMessage("Overriding height limit...."); + break; + } + + case "remove": { + heightLimitExample.removeHeightLimitExample(player); + player.sendMessage("Removing height limit...."); + break; + } + + case "reset": { + heightLimitExample.resetHeightLimitsExample(player); + player.sendMessage("Resetting height limits..."); + break; + } + + default: { + player.sendMessage("Usage: /heightlimit "); + break; + } + } + + return true; + } +} diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java new file mode 100644 index 00000000..9246625f --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/HeightLimitExample.java @@ -0,0 +1,37 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.module.impl; + +import com.lunarclient.apollo.example.module.ApolloModuleExample; +import org.bukkit.entity.Player; + +public abstract class HeightLimitExample extends ApolloModuleExample { + + public abstract void overrideHeightLimitExample(Player viewer); + + public abstract void removeHeightLimitExample(Player viewer); + + public abstract void resetHeightLimitsExample(Player viewer); + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java index 391c46d7..4ffba9cd 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java @@ -36,6 +36,7 @@ import com.lunarclient.apollo.example.json.module.CosmeticJsonExample; import com.lunarclient.apollo.example.json.module.EntityJsonExample; import com.lunarclient.apollo.example.json.module.GlowJsonExample; +import com.lunarclient.apollo.example.json.module.HeightLimitJsonExample; import com.lunarclient.apollo.example.json.module.HologramJsonExample; import com.lunarclient.apollo.example.json.module.LimbJsonExample; import com.lunarclient.apollo.example.json.module.MarkerJsonExample; @@ -81,6 +82,7 @@ public void registerModuleExamples() { this.setCooldownExample(new CooldownJsonExample()); this.setEntityExample(new EntityJsonExample()); this.setGlowExample(new GlowJsonExample()); + this.setHeightLimitExample(new HeightLimitJsonExample()); this.setHologramExample(new HologramJsonExample()); this.setLimbExample(new LimbJsonExample()); this.setMarkerExample(new MarkerJsonExample()); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java new file mode 100644 index 00000000..37f8e89d --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/HeightLimitJsonExample.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.AdventureUtil; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.module.impl.HeightLimitExample; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class HeightLimitJsonExample extends HeightLimitExample { + + @Override + public void overrideHeightLimitExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage"); + message.addProperty("world", "world_the_end"); + message.addProperty("limit", 150); + message.addProperty("display_name_adventure_json_lines", AdventureUtil.toJson( + Component.text("The End", NamedTextColor.DARK_PURPLE) + )); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeHeightLimitExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage"); + message.addProperty("world", "world_the_end"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetHeightLimitsExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); + } + +} diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index 334fe2a4..e92e59a4 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -32,6 +32,8 @@ commands: description: "Glint!" glow: description: "Glow!" + heightlimit: + description: "Height Limit!" hologram: description: "Holograms!" inventory: diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java index 94578ad4..5c0b2a5c 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java @@ -36,6 +36,7 @@ import com.lunarclient.apollo.example.proto.module.CosmeticProtoExample; import com.lunarclient.apollo.example.proto.module.EntityProtoExample; import com.lunarclient.apollo.example.proto.module.GlowProtoExample; +import com.lunarclient.apollo.example.proto.module.HeightLimitProtoExample; import com.lunarclient.apollo.example.proto.module.HologramProtoExample; import com.lunarclient.apollo.example.proto.module.LimbProtoExample; import com.lunarclient.apollo.example.proto.module.MarkerProtoExample; @@ -81,6 +82,7 @@ public void registerModuleExamples() { this.setCooldownExample(new CooldownProtoExample()); this.setEntityExample(new EntityProtoExample()); this.setGlowExample(new GlowProtoExample()); + this.setHeightLimitExample(new HeightLimitProtoExample()); this.setHologramExample(new HologramProtoExample()); this.setLimbExample(new LimbProtoExample()); this.setMarkerExample(new MarkerProtoExample()); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java new file mode 100644 index 00000000..babbce5b --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/HeightLimitProtoExample.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module; + +import com.lunarclient.apollo.example.module.impl.HeightLimitExample; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.heightlimit.v1.OverrideHeightLimitMessage; +import com.lunarclient.apollo.heightlimit.v1.RemoveHeightLimitMessage; +import com.lunarclient.apollo.heightlimit.v1.ResetHeightLimitsMessage; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public class HeightLimitProtoExample extends HeightLimitExample { + + @Override + public void overrideHeightLimitExample(Player viewer) { + OverrideHeightLimitMessage message = OverrideHeightLimitMessage.newBuilder() + .setWorld("world_the_end") + .setLimit(150) + .setDisplayNameAdventureJsonLines(AdventureUtil.toJson( + Component.text("The End", NamedTextColor.DARK_PURPLE) + )) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeHeightLimitExample(Player viewer) { + RemoveHeightLimitMessage message = RemoveHeightLimitMessage.newBuilder() + .setWorld("world_the_end") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetHeightLimitsExample(Player viewer) { + ResetHeightLimitsMessage message = ResetHeightLimitsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); + } + +} diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index f74f917e..468f5d87 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -32,6 +32,8 @@ commands: description: "Glint!" glow: description: "Glow!" + heightlimit: + description: "Height Limit!" hologram: description: "Holograms!" inventory: diff --git a/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java b/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java index 93c11e7a..04176482 100644 --- a/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java +++ b/extra/adventure4/src/main/java/com/lunarclient/apollo/common/ApolloComponent.java @@ -68,6 +68,30 @@ public static String toLegacy(@NonNull Component component) { return LegacyComponentSerializer.legacySection().serialize(component); } + /** + * Returns a new component from the provided legacy {@link String}, + * using {@code &} color codes. + * + * @param legacy the legacy string for this component + * @return the component from the legacy string + * @since 1.2.9 + */ + public static Component fromLegacyAmpersand(@NonNull String legacy) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(legacy); + } + + /** + * Returns this component as a legacy {@link String}, + * using {@code &} color codes. + * + * @param component the component to make into a legacy string + * @return the legacy string for this component + * @since 1.2.9 + */ + public static String toLegacyAmpersand(@NonNull Component component) { + return LegacyComponentSerializer.legacyAmpersand().serialize(component); + } + private ApolloComponent() { } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d83b4d64..8f52700a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ geantyref = "1.3.11" idea = "1.1.7" jetbrains = "24.0.1" lombok = "1.18.38" -protobuf = "0.2.0" +protobuf = "0.2.1" gson = "2.10.1" shadow = "9.4.1" spotless = "8.4.0" diff --git a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java index 6de2e988..c485822d 100644 --- a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java +++ b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java @@ -50,6 +50,8 @@ import com.lunarclient.apollo.module.glint.GlintModule; import com.lunarclient.apollo.module.glow.GlowModule; import com.lunarclient.apollo.module.glow.GlowModuleImpl; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.inventory.InventoryModule; @@ -149,6 +151,7 @@ public void onEnable() { .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(GlintModule.class) .addModule(GlowModule.class, new GlowModuleImpl()) + .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) .addModule(InventoryModule.class) .addModule(LimbModule.class, new LimbModuleImpl()) diff --git a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java index a5f50d27..74215459 100644 --- a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java +++ b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java @@ -46,6 +46,8 @@ import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; @@ -131,6 +133,7 @@ public void onEnable() { .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) .addModule(EntityModule.class, new EntityModuleImpl()) + .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) diff --git a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java index 19a4878b..5255e8e3 100644 --- a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java +++ b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java @@ -48,6 +48,8 @@ import com.lunarclient.apollo.module.entity.EntityModuleImpl; import com.lunarclient.apollo.module.glow.GlowModule; import com.lunarclient.apollo.module.glow.GlowModuleImpl; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; @@ -138,6 +140,7 @@ public void onEnable() { .addModule(CooldownModule.class, new CooldownModuleImpl()) .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(GlowModule.class, new GlowModuleImpl()) + .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java index 8c2e26ab..b1d97ad7 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -49,6 +49,8 @@ import com.lunarclient.apollo.module.glint.GlintModule; import com.lunarclient.apollo.module.glow.GlowModule; import com.lunarclient.apollo.module.glow.GlowModuleImpl; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.inventory.InventoryModule; @@ -171,6 +173,7 @@ public static void init(ApolloMinestomProperties properties) { .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(GlintModule.class) .addModule(GlowModule.class, new GlowModuleImpl()) + .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) .addModule(InventoryModule.class) .addModule(LimbModule.class, new LimbModuleImpl()) diff --git a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java index 8d3ed35f..3b801511 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -46,6 +46,8 @@ import com.lunarclient.apollo.module.cosmetic.CosmeticModuleImpl; import com.lunarclient.apollo.module.entity.EntityModule; import com.lunarclient.apollo.module.entity.EntityModuleImpl; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModule; +import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; @@ -198,6 +200,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { .addModule(CombatModule.class) .addModule(CooldownModule.class, new CooldownModuleImpl()) .addModule(EntityModule.class, new EntityModuleImpl()) + .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) From e1592bb8e11a4b477de7fb7635820e0a67cb8272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 10 Aug 2026 23:04:22 +0200 Subject: [PATCH 3/9] Document snake_case custom data keys (#301) --- docs/developers/modules/inventory.mdx | 12 +++++----- .../example/module/impl/InventoryExample.java | 24 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/developers/modules/inventory.mdx b/docs/developers/modules/inventory.mdx index 35615fe2..62b9a65a 100644 --- a/docs/developers/modules/inventory.mdx +++ b/docs/developers/modules/inventory.mdx @@ -34,27 +34,27 @@ Explore each integration by cycling through each tab, to find the best fit for y **Copy To Clipboard Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:paper",Count:1b,components:{"minecraft:custom_name":"COPY TO CLIPBOARD","minecraft:custom_data":{lunar:{unclickable:true,copyToClipboard:"lunarclient.com"}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:paper",Count:1b,components:{"minecraft:custom_name":"COPY TO CLIPBOARD","minecraft:custom_data":{lunar:{unclickable:true,copy_to_clipboard:"lunarclient.com"}}}}}` **Open URL Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:torch",Count:1b,components:{"minecraft:custom_name":"OPEN URL","minecraft:custom_data":{lunar:{unclickable:true,openUrl:"https://lunarclient.com"}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:torch",Count:1b,components:{"minecraft:custom_name":"OPEN URL","minecraft:custom_data":{lunar:{unclickable:true,open_url:"https://lunarclient.com"}}}}}` **Suggest Command Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:book",Count:1b,components:{"minecraft:custom_name":"SUGGEST COMMAND","minecraft:custom_data":{lunar:{unclickable:true,suggestCommand:"/apollo"}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:book",Count:1b,components:{"minecraft:custom_name":"SUGGEST COMMAND","minecraft:custom_data":{lunar:{unclickable:true,suggest_command:"/apollo"}}}}}` **Run Command Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:writable_book",Count:1b,components:{"minecraft:custom_name":"RUN COMMAND","minecraft:custom_data":{lunar:{unclickable:true,runCommand:"/apollo"}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:writable_book",Count:1b,components:{"minecraft:custom_name":"RUN COMMAND","minecraft:custom_data":{lunar:{unclickable:true,run_command:"/apollo"}}}}}` **Hide Item Tooltip Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:sponge",Count:1b,components:{"minecraft:custom_name":"HIDE ITEM TOOLTIP","minecraft:custom_data":{lunar:{unclickable:true,hideItemTooltip:true}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:sponge",Count:1b,components:{"minecraft:custom_name":"HIDE ITEM TOOLTIP","minecraft:custom_data":{lunar:{unclickable:true,hide_item_tooltip:true}}}}}` **Hide Slot Highlight Item** -`/summon item ~ ~1 ~ {Item:{id:"minecraft:dirt",Count:1b,components:{"minecraft:custom_name":"HIDE SLOT HIGHTLIGHT","minecraft:custom_data":{lunar:{unclickable:true,hideSlotHighlight:true}}}}}` +`/summon item ~ ~1 ~ {Item:{id:"minecraft:dirt",Count:1b,components:{"minecraft:custom_name":"HIDE SLOT HIGHTLIGHT","minecraft:custom_data":{lunar:{unclickable:true,hide_slot_highlight:true}}}}}` diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java index 37e62f7c..5d5ed71a 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java @@ -46,12 +46,12 @@ public boolean inventoryModuleExample(Player player) { public void inventoryModuleCommandExample(Player player) { player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:stone\",Count:1b,components:{\"minecraft:custom_name\":\"UNCLICKABLE\",\"minecraft:custom_data\":{lunar:{unclickable:true}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:paper\",Count:1b,components:{\"minecraft:custom_name\":\"COPY TO CLIPBOARD\",\"minecraft:custom_data\":{lunar:{unclickable:true,copyToClipboard:\"lunarclient.com\"}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:torch\",Count:1b,components:{\"minecraft:custom_name\":\"OPEN URL\",\"minecraft:custom_data\":{lunar:{unclickable:true,openUrl:\"https://lunarclient.com\"}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:book\",Count:1b,components:{\"minecraft:custom_name\":\"SUGGEST COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,suggestCommand:\"/apollo\"}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:writable_book\",Count:1b,components:{\"minecraft:custom_name\":\"RUN COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,runCommand:\"/apollo\"}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:sponge\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE ITEM TOOLTIP\",\"minecraft:custom_data\":{lunar:{unclickable:true,hideItemTooltip:true}}}}}"); - player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:dirt\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE SLOT HIGHTLIGHT\",\"minecraft:custom_data\":{lunar:{unclickable:true,hideSlotHighlight:true}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:paper\",Count:1b,components:{\"minecraft:custom_name\":\"COPY TO CLIPBOARD\",\"minecraft:custom_data\":{lunar:{unclickable:true,copy_to_clipboard:\"lunarclient.com\"}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:torch\",Count:1b,components:{\"minecraft:custom_name\":\"OPEN URL\",\"minecraft:custom_data\":{lunar:{unclickable:true,open_url:\"https://lunarclient.com\"}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:book\",Count:1b,components:{\"minecraft:custom_name\":\"SUGGEST COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,suggest_command:\"/apollo\"}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:writable_book\",Count:1b,components:{\"minecraft:custom_name\":\"RUN COMMAND\",\"minecraft:custom_data\":{lunar:{unclickable:true,run_command:\"/apollo\"}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:sponge\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE ITEM TOOLTIP\",\"minecraft:custom_data\":{lunar:{unclickable:true,hide_item_tooltip:true}}}}}"); + player.performCommand("summon item ~ ~1 ~ {Item:{id:\"minecraft:dirt\",Count:1b,components:{\"minecraft:custom_name\":\"HIDE SLOT HIGHTLIGHT\",\"minecraft:custom_data\":{lunar:{unclickable:true,hide_slot_highlight:true}}}}}"); } public void inventoryModuleNMSExample(Player player) { @@ -70,7 +70,7 @@ public void inventoryModuleNMSExample(Player player) { ); copyToClipboardItem = ItemUtil.addTag(copyToClipboardItem, "unclickable", true); - inventory.setItem(12, ItemUtil.addTag(copyToClipboardItem, "copyToClipboard", "lunarclient.com")); + inventory.setItem(12, ItemUtil.addTag(copyToClipboardItem, "copy_to_clipboard", "lunarclient.com")); ItemStack openUrlItem = ItemUtil.itemWithName( Material.TORCH, @@ -78,7 +78,7 @@ public void inventoryModuleNMSExample(Player player) { ); openUrlItem = ItemUtil.addTag(openUrlItem, "unclickable", true); - inventory.setItem(14, ItemUtil.addTag(openUrlItem, "openUrl", "https://lunarclient.com")); + inventory.setItem(14, ItemUtil.addTag(openUrlItem, "open_url", "https://lunarclient.com")); ItemStack suggestCommandItem = ItemUtil.itemWithName( Material.BOOK, @@ -86,7 +86,7 @@ public void inventoryModuleNMSExample(Player player) { ); suggestCommandItem = ItemUtil.addTag(suggestCommandItem, "unclickable", true); - inventory.setItem(16, ItemUtil.addTag(suggestCommandItem, "suggestCommand", "/apollo")); + inventory.setItem(16, ItemUtil.addTag(suggestCommandItem, "suggest_command", "/apollo")); ItemStack runCommandItem = ItemUtil.itemWithName( Material.ENCHANTED_BOOK, @@ -94,7 +94,7 @@ public void inventoryModuleNMSExample(Player player) { ); runCommandItem = ItemUtil.addTag(runCommandItem, "unclickable", true); - inventory.setItem(29, ItemUtil.addTag(runCommandItem, "runCommand", "/apollo")); + inventory.setItem(29, ItemUtil.addTag(runCommandItem, "run_command", "/apollo")); ItemStack hideTooltipItem = ItemUtil.itemWithName( Material.SPONGE, @@ -102,7 +102,7 @@ public void inventoryModuleNMSExample(Player player) { ); hideTooltipItem = ItemUtil.addTag(hideTooltipItem, "unclickable", true); - inventory.setItem(31, ItemUtil.addTag(hideTooltipItem, "hideItemTooltip", true)); + inventory.setItem(31, ItemUtil.addTag(hideTooltipItem, "hide_item_tooltip", true)); ItemStack hideHighlightItem = ItemUtil.itemWithName( Material.DIRT, @@ -110,7 +110,7 @@ public void inventoryModuleNMSExample(Player player) { ); hideHighlightItem = ItemUtil.addTag(hideHighlightItem, "unclickable", true); - inventory.setItem(33, ItemUtil.addTag(hideHighlightItem, "hideSlotHighlight", true)); + inventory.setItem(33, ItemUtil.addTag(hideHighlightItem, "hide_slot_highlight", true)); player.openInventory(inventory); } From e5a2b0bbceba00a365073eccee119228c18068d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 10 Aug 2026 23:05:15 +0200 Subject: [PATCH 4/9] Add `TransferModule#ping` validation & increase `PingRequest` timeout to 10s (#302) --- .../apollo/module/transfer/PingRequest.java | 11 +++++++++++ .../apollo/module/transfer/TransferModule.java | 12 ++++++++++++ .../lunarclient/apollo/roundtrip/ApolloRequest.java | 10 ++++++++++ .../apollo/roundtrip/ApolloRoundtripManager.java | 5 +++-- .../apollo/module/transfer/TransferModuleImpl.java | 13 ++++++++++++- docs/developers/modules/transfer.mdx | 2 ++ example/bukkit/json/src/main/resources/plugin.yml | 2 ++ example/bukkit/proto/src/main/resources/plugin.yml | 2 ++ 8 files changed, 54 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java b/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java index 27640400..3b305bd3 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java +++ b/api/src/main/java/com/lunarclient/apollo/module/transfer/PingRequest.java @@ -45,4 +45,15 @@ public final class PingRequest extends ApolloRequest { */ List serverIps; + /** + * Returns the timeout for ping requests, in milliseconds. + * + * @return the request timeout, in milliseconds + * @since 1.2.9 + */ + @Override + public long getTimeoutMillis() { + return 10_000L; + } + } diff --git a/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java b/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java index 2d033f4d..39b08ed8 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/transfer/TransferModule.java @@ -42,6 +42,14 @@ @ModuleDefinition(id = "transfer", name = "Transfer") public abstract class TransferModule extends ApolloModule { + /** + * The maximum amount of server IPs the client will ping + * for a single {@link PingRequest}. + * + * @since 1.2.9 + */ + public static final int MAX_PINGS_PER_PACKET = 10; + @Override public Collection getSupportedPlatforms() { return Arrays.asList(ApolloPlatform.Kind.SERVER, ApolloPlatform.Kind.PROXY); @@ -55,6 +63,8 @@ public Collection getSupportedPlatforms() { * @param player the player * @param serverIps all server IPs to ping * @return future to be listened to for errors/success + * @throws IllegalArgumentException if no server IPs or more than + * {@value #MAX_PINGS_PER_PACKET} server IPs are provided * @since 1.0.0 */ public Future ping(ApolloPlayer player, List serverIps) { @@ -85,6 +95,8 @@ public Future transfer(ApolloPlayer player, String serverIp) { * @param player the player * @param request the ping request * @return future to be listened to for errors/success + * @throws IllegalArgumentException if no server IPs or more than + * {@value #MAX_PINGS_PER_PACKET} server IPs are provided * @since 1.0.0 */ public abstract Future ping(ApolloPlayer player, PingRequest request); diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java index 372d8ae3..f5bb25b9 100644 --- a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java +++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRequest.java @@ -67,4 +67,14 @@ public ApolloRequest() { this.sentTime = System.currentTimeMillis(); } + /** + * Returns the time to wait for a response, in milliseconds. + * + * @return the request timeout, in milliseconds + * @since 1.2.9 + */ + public long getTimeoutMillis() { + return TIMEOUT; + } + } diff --git a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java index f0d5bce8..31771c4c 100644 --- a/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java +++ b/api/src/main/java/com/lunarclient/apollo/roundtrip/ApolloRoundtripManager.java @@ -119,13 +119,14 @@ public void registerListener(ApolloRequest request this.paginationManager.handleTimeout(packetId); if (listener != null) { - Throwable error = new Throwable("Timeout exceeded!"); + Throwable error = new Throwable("Timeout exceeded! No " + request.getClass().getSimpleName() + + " response received within " + request.getTimeoutMillis() + "ms"); future.handleFailure(error); } } catch (Exception e) { e.printStackTrace(); } - }, ApolloRequest.TIMEOUT, TimeUnit.MILLISECONDS); + }, request.getTimeoutMillis(), TimeUnit.MILLISECONDS); this.listeners.put(packetId, (UncertainFuture) future); } diff --git a/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java index 6971cf56..febd0109 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/transfer/TransferModuleImpl.java @@ -53,9 +53,20 @@ public TransferModuleImpl() { @Override public Future ping(@NonNull ApolloPlayer player, @NonNull PingRequest request) { + List serverIps = request.getServerIps(); + + if (serverIps == null || serverIps.isEmpty()) { + throw new IllegalArgumentException("PingRequest must contain at least 1 server IP!"); + } + + if (serverIps.size() > MAX_PINGS_PER_PACKET) { + throw new IllegalArgumentException("PingRequest supports up to " + MAX_PINGS_PER_PACKET + + " server IPs, got " + serverIps.size() + "!"); + } + com.lunarclient.apollo.transfer.v1.PingRequest requestProto = com.lunarclient.apollo.transfer.v1.PingRequest.newBuilder() .setRequestId(ByteString.copyFromUtf8(request.getRequestId().toString())) - .addAllServerIps(request.getServerIps()) + .addAllServerIps(serverIps) .build(); return ((AbstractApolloPlayer) player).sendRoundTripPacket(request, requestProto); diff --git a/docs/developers/modules/transfer.mdx b/docs/developers/modules/transfer.mdx index b2ea55b0..b099d973 100644 --- a/docs/developers/modules/transfer.mdx +++ b/docs/developers/modules/transfer.mdx @@ -119,6 +119,7 @@ public void transferExample(Player viewer) { You can provide up to `10` different addresses per ping packet. + Requests with more addresses are rejected with an `IllegalArgumentException`. @@ -209,6 +210,7 @@ public void transferExample(Player player) { You can provide up to `10` different addresses per ping packet. + Addresses beyond the first `10` are reported as `STATUS_TIMED_OUT` instead of being pinged. ```java diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index e92e59a4..0b4a5910 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -54,6 +54,8 @@ commands: description: "Pay Now!" richpresence: description: "Rich Presence!" + serverlink: + description: "Server Links!" saturation: description: "Saturation!" serverrule: diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index 468f5d87..bab850d2 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -54,6 +54,8 @@ commands: description: "Pay Now!" richpresence: description: "Rich Presence!" + serverlink: + description: "Server Links!" saturation: description: "Saturation!" serverrule: From 464006183ac1798a83d9b0e034e9eb8f48d757d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 10 Aug 2026 23:06:24 +0200 Subject: [PATCH 5/9] example(internal): npc visibility tracking (#303) --- .../api/listener/ApolloPlayerApiListener.java | 5 + .../api/module/CosmeticApiExample.java | 46 ++++++++- .../apollo/example/ApolloExamplePlugin.java | 8 ++ .../example/command/CosmeticCommand.java | 18 +++- .../example/module/impl/CosmeticExample.java | 21 +++- .../listener/ApolloPlayerJsonListener.java | 23 +++++ .../json/module/CosmeticJsonExample.java | 64 ++++++++++++- .../apollo/example/nms/NpcManager.java | 95 ++++++++++++++++--- .../apollo/example/nms/NpcViewerListener.java | 33 +++++++ .../apollo/example/nms/PlayerNpc.java | 17 ++++ .../listener/ApolloPlayerProtoListener.java | 23 +++++ .../proto/module/CosmeticProtoExample.java | 61 +++++++++++- 12 files changed, 391 insertions(+), 23 deletions(-) create mode 100644 example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.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 d0cb1938..068ff039 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/listener/ApolloPlayerApiListener.java @@ -82,6 +82,11 @@ private void onApolloRegister(ApolloRegisterPlayerEvent event) { for (CommandCosmetic spec : npc.getCosmetics()) { cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec); } + + PlayerNpc.ActiveEmote emote = npc.getActiveEmote(); + if (emote != null && npc.getViewers().contains(player.getUniqueId())) { + cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata()); + } } } diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java index 0c82c2ca..98608354 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/CosmeticApiExample.java @@ -175,13 +175,30 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) { } @Override - public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) { + public void startNpcEmoteInternal(UUID npcUuid, int emoteId, int metadata) { + List viewers = this.getApolloViewers(npcUuid); + if (viewers.isEmpty()) { + return; + } + Emote emote = Emote.builder() .id(emoteId) .metadata(metadata) .build(); - this.cosmeticModule.startNpcEmote(Recipients.ofEveryone(), npcUuid, emote); + this.cosmeticModule.startNpcEmote(Recipients.of(viewers), npcUuid, emote); + } + + @Override + public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + Emote emote = Emote.builder() + .id(emoteId) + .metadata(metadata) + .build(); + + this.cosmeticModule.startNpcEmote(apolloPlayer, npcUuid, emote); + }); } @Override @@ -189,6 +206,31 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) { this.cosmeticModule.stopNpcEmote(Recipients.ofEveryone(), npcUuid); } + @Override + public void stopNpcEmoteInternal(UUID npcUuid) { + List viewers = this.getApolloViewers(npcUuid); + if (viewers.isEmpty()) { + return; + } + + this.cosmeticModule.stopNpcEmote(Recipients.of(viewers), npcUuid); + } + + @Override + public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> + this.cosmeticModule.stopNpcEmote(apolloPlayer, npcUuid)); + } + + private List getApolloViewers(UUID npcUuid) { + List viewers = new ArrayList<>(); + for (Player player : this.getNpcViewers(npcUuid)) { + Apollo.getPlayerManager().getPlayer(player.getUniqueId()).ifPresent(viewers::add); + } + + return viewers; + } + @Override public void resetNpcEmotesExample() { this.cosmeticModule.resetNpcEmotes(Recipients.ofEveryone()); diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java index 94092492..433af374 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java @@ -93,6 +93,7 @@ import com.lunarclient.apollo.example.module.impl.VignetteExample; import com.lunarclient.apollo.example.module.impl.WaypointExample; import com.lunarclient.apollo.example.nms.NpcManager; +import com.lunarclient.apollo.example.nms.PlayerNpc; import lombok.Getter; import lombok.Setter; import org.bukkit.plugin.java.JavaPlugin; @@ -153,6 +154,13 @@ public void onEnable() { this.registerCommands(); this.registerModuleExamples(); this.registerListeners(); + + this.npcManager.addViewerListener((viewer, npc) -> { + PlayerNpc.ActiveEmote emote = npc.getActiveEmote(); + if (emote != null && this.cosmeticExample != null) { + this.cosmeticExample.startNpcEmoteToViewer(viewer, npc.getUuid(), emote.getEmoteId(), emote.getMetadata()); + } + }); } @Override diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java index ad8057e8..92817e47 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/CosmeticCommand.java @@ -222,7 +222,12 @@ private boolean handleEmote(Player player, CosmeticExample example, String[] arg } } - example.startNpcEmoteInternal(player, uuid, emoteId, metadata); + example.startNpcEmoteInternal(uuid, emoteId, metadata); + + int emoteMetadata = metadata; + ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(uuid) + .ifPresent(npc -> npc.setActiveEmote(new PlayerNpc.ActiveEmote(emoteId, emoteMetadata))); + player.sendMessage(ChatColor.GREEN + "Started emote " + emoteId + " on NPC " + args[2]); break; } @@ -238,13 +243,22 @@ private boolean handleEmote(Player player, CosmeticExample example, String[] arg return true; } - example.stopNpcEmoteExample(player, uuid); + example.stopNpcEmoteInternal(uuid); + + ApolloExamplePlugin.getInstance().getNpcManager().findByUuid(uuid) + .ifPresent(npc -> npc.setActiveEmote(null)); + player.sendMessage(ChatColor.GREEN + "Stopped emote on NPC " + args[2]); break; } case "reset": { example.resetNpcEmotesExample(); + + for (PlayerNpc npc : ApolloExamplePlugin.getInstance().getNpcManager().getNpcs()) { + npc.setActiveEmote(null); + } + player.sendMessage(ChatColor.GREEN + "Reset all NPC emotes"); break; } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java index 0e56b672..b6e33e20 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/CosmeticExample.java @@ -23,6 +23,7 @@ */ package com.lunarclient.apollo.example.module.impl; +import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.module.ApolloModuleExample; import com.lunarclient.apollo.example.nms.CommandCosmetic; import java.util.Collections; @@ -54,12 +55,30 @@ public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmeti public abstract void startNpcEmoteExample(Player viewer, UUID npcUuid); - public abstract void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata); + public void startNpcEmoteInternal(UUID npcUuid, int emoteId, int metadata) { + for (Player viewer : this.getNpcViewers(npcUuid)) { + this.startNpcEmoteToViewer(viewer, npcUuid, emoteId, metadata); + } + } + + public abstract void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata); public abstract void stopNpcEmoteExample(Player viewer, UUID npcUuid); + public void stopNpcEmoteInternal(UUID npcUuid) { + for (Player viewer : this.getNpcViewers(npcUuid)) { + this.stopNpcEmoteToViewer(viewer, npcUuid); + } + } + + public abstract void stopNpcEmoteToViewer(Player viewer, UUID npcUuid); + public abstract void resetNpcEmotesExample(); + protected List getNpcViewers(UUID npcUuid) { + return ApolloExamplePlugin.getInstance().getNpcManager().getViewers(npcUuid); + } + public abstract void displaySprayExample(Player viewer, int sprayId); public abstract void removeSprayExample(int sprayId); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java index 6c0984d5..217aab0a 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPlayerJsonListener.java @@ -26,6 +26,9 @@ import com.google.gson.JsonObject; import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.nms.CommandCosmetic; +import com.lunarclient.apollo.example.nms.PlayerNpc; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -81,6 +84,26 @@ private void onRegisterChannel(PlayerRegisterChannelEvent event) { PLAYERS_RUNNING_APOLLO.add(player.getUniqueId()); player.sendMessage("You are using LunarClient!"); + + this.applyNpcCosmetics(player); + } + + private void applyNpcCosmetics(Player player) { + CosmeticExample cosmeticExample = this.plugin.getCosmeticExample(); + if (cosmeticExample == null) { + return; + } + + for (PlayerNpc npc : this.plugin.getNpcManager().getNpcs()) { + for (CommandCosmetic spec : npc.getCosmetics()) { + cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec); + } + + PlayerNpc.ActiveEmote emote = npc.getActiveEmote(); + if (emote != null && npc.getViewers().contains(player.getUniqueId())) { + cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata()); + } + } } @EventHandler diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java index cff28169..b9a43ef9 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/CosmeticJsonExample.java @@ -29,6 +29,7 @@ import com.lunarclient.apollo.example.json.util.JsonPacketUtil; import com.lunarclient.apollo.example.json.util.JsonUtil; import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.nms.CommandCosmetic; import java.time.Duration; import java.util.List; import java.util.UUID; @@ -103,6 +104,56 @@ public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List JsonPacketUtil.broadcastPacket(message); } + @Override + public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) { + JsonPacketUtil.broadcastPacket(this.createEquipMessage(npcUuid, cosmetic)); + } + + @Override + public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) { + JsonPacketUtil.sendPacket(viewer, this.createEquipMessage(npcUuid, cosmetic)); + } + + private JsonObject createEquipMessage(UUID npcUuid, CommandCosmetic cosmetic) { + JsonObject cosmeticObject = new JsonObject(); + cosmeticObject.addProperty("id", cosmetic.getId()); + + CommandCosmetic.Options options = cosmetic.getOptions(); + if (options instanceof CommandCosmetic.Hat) { + CommandCosmetic.Hat hat = (CommandCosmetic.Hat) options; + JsonObject hatOptions = new JsonObject(); + hatOptions.addProperty("show_over_helmet", hat.isShowOverHelmet()); + hatOptions.addProperty("show_over_skin_layer", hat.isShowOverSkinLayer()); + hatOptions.addProperty("height_offset", hat.getHeightOffset()); + cosmeticObject.add("hat_options", hatOptions); + } else if (options instanceof CommandCosmetic.Cloak) { + JsonObject cloakOptions = new JsonObject(); + cloakOptions.addProperty("use_cloth_physics", ((CommandCosmetic.Cloak) options).isUseClothPhysics()); + cosmeticObject.add("cloak_options", cloakOptions); + } else if (options instanceof CommandCosmetic.Pet) { + JsonObject petOptions = new JsonObject(); + petOptions.addProperty("flip_shoulder", ((CommandCosmetic.Pet) options).isFlipShoulder()); + cosmeticObject.add("pet_options", petOptions); + } else if (options instanceof CommandCosmetic.Body) { + CommandCosmetic.Body body = (CommandCosmetic.Body) options; + JsonObject bodyOptions = new JsonObject(); + bodyOptions.addProperty("show_over_chestplate", body.isShowOverChestplate()); + bodyOptions.addProperty("show_over_leggings", body.isShowOverLeggings()); + bodyOptions.addProperty("show_over_boots", body.isShowOverBoots()); + cosmeticObject.add("body_options", bodyOptions); + } + + JsonArray cosmeticsArray = new JsonArray(); + cosmeticsArray.add(cosmeticObject); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + message.add("cosmetics", cosmeticsArray); + + return message; + } + @Override public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); @@ -154,7 +205,7 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) { } @Override - public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) { + public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) { JsonObject emote = new JsonObject(); emote.addProperty("id", emoteId); emote.addProperty("metadata", metadata); @@ -164,7 +215,7 @@ public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); message.add("emote", emote); - JsonPacketUtil.broadcastPacket(message); + JsonPacketUtil.sendPacket(viewer, message); } @Override @@ -176,6 +227,15 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) { JsonPacketUtil.broadcastPacket(message); } + @Override + public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.cosmetic.v1.StopNpcEmoteMessage"); + message.add("npc_uuid", JsonUtil.createUuidObject(npcUuid)); + + JsonPacketUtil.sendPacket(viewer, message); + } + @Override public void resetNpcEmotesExample() { JsonObject message = new JsonObject(); diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java index be00b996..37f2e2e5 100644 --- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcManager.java @@ -26,6 +26,7 @@ import com.mojang.authlib.GameProfile; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -51,10 +52,12 @@ import org.bukkit.World; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Nullable; @@ -65,7 +68,11 @@ public final class NpcManager implements Listener { HumanoidArm.RIGHT, false, false, ParticleStatus.ALL ); + private static final double TRACKING_RANGE = 48.0; + private static final double TRACKING_RANGE_SQUARED = TRACKING_RANGE * TRACKING_RANGE; + private final Map npcs = new HashMap<>(); + private final List viewerListeners = new ArrayList<>(); private final JavaPlugin plugin; private final NpcStore store; @@ -75,6 +82,11 @@ public NpcManager(JavaPlugin plugin) { Bukkit.getPluginManager().registerEvents(this, plugin); Bukkit.getScheduler().runTask(plugin, this::loadOrSpawnDefaults); + Bukkit.getScheduler().runTaskTimer(plugin, this::updateVisibility, 1L, 10L); + } + + public void addViewerListener(NpcViewerListener listener) { + this.viewerListeners.add(listener); } public void removeNpc(UUID uuid) { @@ -114,21 +126,73 @@ public Collection getNpcs() { } @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle(); - for (PlayerNpc npc : this.npcs.values()) { - this.showNpc(viewer, npc); - } + public void onPlayerQuit(PlayerQuitEvent event) { + this.forgetViewer(event.getPlayer()); } @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { - ServerPlayer viewer = ((CraftPlayer) event.getPlayer()).getHandle(); + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + this.forgetViewer(event.getPlayer()); + } + + @EventHandler + public void onPlayerRespawn(PlayerRespawnEvent event) { + this.forgetViewer(event.getPlayer()); + } + + private void forgetViewer(Player player) { for (PlayerNpc npc : this.npcs.values()) { - this.hideNpc(viewer, npc); + npc.getViewers().remove(player.getUniqueId()); + } + } + + private void updateVisibility() { + for (Player player : Bukkit.getOnlinePlayers()) { + for (PlayerNpc npc : this.npcs.values()) { + boolean inRange = this.isWithinTrackingRange(player, npc); + boolean viewing = npc.getViewers().contains(player.getUniqueId()); + + if (inRange && !viewing) { + npc.getViewers().add(player.getUniqueId()); + this.showNpc(((CraftPlayer) player).getHandle(), npc); + + for (NpcViewerListener listener : this.viewerListeners) { + listener.onNpcShown(player, npc); + } + } else if (!inRange && viewing) { + npc.getViewers().remove(player.getUniqueId()); + this.hideNpc(((CraftPlayer) player).getHandle(), npc); + } + } } } + private boolean isWithinTrackingRange(Player player, PlayerNpc npc) { + Location location = npc.getLocation(); + World world = location.getWorld(); + + return world != null + && world.equals(player.getWorld()) + && player.getLocation().distanceSquared(location) <= NpcManager.TRACKING_RANGE_SQUARED; + } + + public List getViewers(UUID npcUuid) { + PlayerNpc npc = this.npcs.get(npcUuid); + if (npc == null) { + return Collections.emptyList(); + } + + List viewers = new ArrayList<>(); + for (UUID viewerUuid : npc.getViewers()) { + Player player = Bukkit.getPlayer(viewerUuid); + if (player != null) { + viewers.add(player); + } + } + + return viewers; + } + private void loadOrSpawnDefaults() { if (!this.store.exists()) { this.spawnDefaultNpcs(); @@ -184,9 +248,7 @@ private void spawnDefaultNpcs() { PlayerNpc playerNpc = new PlayerNpc(npc.getUUID(), name, location.clone(), npc); this.npcs.put(playerNpc.getUuid(), playerNpc); - for (ServerPlayer viewer : server.getPlayerList().getPlayers()) { - this.showNpc(viewer, playerNpc); - } + this.updateVisibility(); return playerNpc; } @@ -220,9 +282,14 @@ private void hideNpc(ServerPlayer viewer, PlayerNpc npc) { } private void despawnNpcs(PlayerNpc npc) { - for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { - this.hideNpc(player, npc); + for (UUID viewerUuid : npc.getViewers()) { + Player player = Bukkit.getPlayer(viewerUuid); + if (player != null) { + this.hideNpc(((CraftPlayer) player).getHandle(), npc); + } } + + npc.getViewers().clear(); } } diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java new file mode 100644 index 00000000..479e3eeb --- /dev/null +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/NpcViewerListener.java @@ -0,0 +1,33 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.nms; + +import org.bukkit.entity.Player; + +@FunctionalInterface +public interface NpcViewerListener { + + void onNpcShown(Player viewer, PlayerNpc npc); + +} diff --git a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java index 9bdde6f9..fa464bc3 100644 --- a/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java +++ b/example/bukkit/nms/src/main/java/com/lunarclient/apollo/example/nms/PlayerNpc.java @@ -24,13 +24,16 @@ package com.lunarclient.apollo.example.nms; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.minecraft.server.level.ServerPlayer; import org.bukkit.Location; +import org.jetbrains.annotations.Nullable; @Getter @RequiredArgsConstructor @@ -40,12 +43,26 @@ public final class PlayerNpc { private final String name; private final Location location; private final ServerPlayer handle; + private final Set viewers = new HashSet<>(); @Setter private List cosmetics = new ArrayList<>(); + @Setter + @Nullable + private ActiveEmote activeEmote; + public int getEntityId() { return this.handle.getId(); } + @Getter + @RequiredArgsConstructor + public static final class ActiveEmote { + + private final int emoteId; + private final int metadata; + + } + } diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java index a9aee632..6e55ce2a 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPlayerProtoListener.java @@ -24,6 +24,9 @@ package com.lunarclient.apollo.example.proto.listener; import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.nms.CommandCosmetic; +import com.lunarclient.apollo.example.nms.PlayerNpc; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; import com.lunarclient.apollo.player.v1.UpdatePlayerWorldMessage; import java.util.HashSet; @@ -79,6 +82,26 @@ private void onRegisterChannel(PlayerRegisterChannelEvent event) { PLAYERS_RUNNING_APOLLO.add(player.getUniqueId()); player.sendMessage("You are using LunarClient!"); + + this.applyNpcCosmetics(player); + } + + private void applyNpcCosmetics(Player player) { + CosmeticExample cosmeticExample = this.plugin.getCosmeticExample(); + if (cosmeticExample == null) { + return; + } + + for (PlayerNpc npc : this.plugin.getNpcManager().getNpcs()) { + for (CommandCosmetic spec : npc.getCosmetics()) { + cosmeticExample.equipNpcCosmeticToViewer(player, npc.getUuid(), spec); + } + + PlayerNpc.ActiveEmote emote = npc.getActiveEmote(); + if (emote != null && npc.getViewers().contains(player.getUniqueId())) { + cosmeticExample.startNpcEmoteToViewer(player, npc.getUuid(), emote.getEmoteId(), emote.getMetadata()); + } + } } @EventHandler diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java index e83851d7..f66f03aa 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/CosmeticProtoExample.java @@ -24,11 +24,13 @@ package com.lunarclient.apollo.example.proto.module; import com.google.common.collect.Lists; +import com.lunarclient.apollo.cosmetic.v1.BodyOptions; import com.lunarclient.apollo.cosmetic.v1.CloakOptions; import com.lunarclient.apollo.cosmetic.v1.Cosmetic; import com.lunarclient.apollo.cosmetic.v1.DisplaySprayMessage; import com.lunarclient.apollo.cosmetic.v1.Emote; import com.lunarclient.apollo.cosmetic.v1.EquipNpcCosmeticsMessage; +import com.lunarclient.apollo.cosmetic.v1.HatOptions; import com.lunarclient.apollo.cosmetic.v1.PetOptions; import com.lunarclient.apollo.cosmetic.v1.RemoveSprayMessage; import com.lunarclient.apollo.cosmetic.v1.ResetNpcCosmeticsMessage; @@ -38,6 +40,7 @@ import com.lunarclient.apollo.cosmetic.v1.StopNpcEmoteMessage; import com.lunarclient.apollo.cosmetic.v1.UnequipNpcCosmeticsMessage; import com.lunarclient.apollo.example.module.impl.CosmeticExample; +import com.lunarclient.apollo.example.nms.CommandCosmetic; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; import com.lunarclient.apollo.example.proto.util.ProtobufUtil; import com.lunarclient.apollo.packetenrichment.v1.Direction; @@ -109,6 +112,51 @@ public void equipNpcCosmeticsInternal(Player viewer, UUID npcUuid, List ProtobufPacketUtil.broadcastPacket(message); } + @Override + public void equipNpcCosmeticInternal(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) { + ProtobufPacketUtil.broadcastPacket(this.createEquipMessage(npcUuid, cosmetic)); + } + + @Override + public void equipNpcCosmeticToViewer(Player viewer, UUID npcUuid, CommandCosmetic cosmetic) { + ProtobufPacketUtil.sendPacket(viewer, this.createEquipMessage(npcUuid, cosmetic)); + } + + private EquipNpcCosmeticsMessage createEquipMessage(UUID npcUuid, CommandCosmetic cosmetic) { + Cosmetic.Builder cosmeticBuilder = Cosmetic.newBuilder() + .setId(cosmetic.getId()); + + CommandCosmetic.Options options = cosmetic.getOptions(); + if (options instanceof CommandCosmetic.Hat) { + CommandCosmetic.Hat hat = (CommandCosmetic.Hat) options; + cosmeticBuilder.setHatOptions(HatOptions.newBuilder() + .setShowOverHelmet(hat.isShowOverHelmet()) + .setShowOverSkinLayer(hat.isShowOverSkinLayer()) + .setHeightOffset(hat.getHeightOffset()) + .build()); + } else if (options instanceof CommandCosmetic.Cloak) { + cosmeticBuilder.setCloakOptions(CloakOptions.newBuilder() + .setUseClothPhysics(((CommandCosmetic.Cloak) options).isUseClothPhysics()) + .build()); + } else if (options instanceof CommandCosmetic.Pet) { + cosmeticBuilder.setPetOptions(PetOptions.newBuilder() + .setFlipShoulder(((CommandCosmetic.Pet) options).isFlipShoulder()) + .build()); + } else if (options instanceof CommandCosmetic.Body) { + CommandCosmetic.Body body = (CommandCosmetic.Body) options; + cosmeticBuilder.setBodyOptions(BodyOptions.newBuilder() + .setShowOverChestplate(body.isShowOverChestplate()) + .setShowOverLeggings(body.isShowOverLeggings()) + .setShowOverBoots(body.isShowOverBoots()) + .build()); + } + + return EquipNpcCosmeticsMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .addCosmetics(cosmeticBuilder.build()) + .build(); + } + @Override public void unequipNpcCosmeticsExample(Player viewer, UUID npcUuid) { List cosmeticIds = Lists.newArrayList(434, 3654, 5095, 3, 3977); @@ -153,7 +201,7 @@ public void startNpcEmoteExample(Player viewer, UUID npcUuid) { } @Override - public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int metadata) { + public void startNpcEmoteToViewer(Player viewer, UUID npcUuid, int emoteId, int metadata) { StartNpcEmoteMessage message = StartNpcEmoteMessage.newBuilder() .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) .setEmote(Emote.newBuilder() @@ -162,7 +210,7 @@ public void startNpcEmoteInternal(Player viewer, UUID npcUuid, int emoteId, int .build()) .build(); - ProtobufPacketUtil.broadcastPacket(message); + ProtobufPacketUtil.sendPacket(viewer, message); } @Override @@ -174,6 +222,15 @@ public void stopNpcEmoteExample(Player viewer, UUID npcUuid) { ProtobufPacketUtil.broadcastPacket(message); } + @Override + public void stopNpcEmoteToViewer(Player viewer, UUID npcUuid) { + StopNpcEmoteMessage message = StopNpcEmoteMessage.newBuilder() + .setNpcUuid(ProtobufUtil.createUuidProto(npcUuid)) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + @Override public void resetNpcEmotesExample() { ResetNpcEmotesMessage message = ResetNpcEmotesMessage.getDefaultInstance(); From d653236c7d75de46a55411861a947a9e8cff2b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 17 Aug 2026 14:41:05 +0200 Subject: [PATCH 6/9] Feature - Inventory & Chat Buttons (#304) * Inventory & Chat Buttons # Conflicts: # gradle/libs.versions.toml * Add default chat & inventory buttons config option * Add button docs * Add button images to docs * Update inventory.mdx * Update chat.mdx * Remove temp gradle publish --------- Co-authored-by: Trentin <25537885+TrentinTheKid@users.noreply.github.com> --- .../lunarclient/apollo/ApolloPlatform.java | 31 + .../apollo/common/button/ApolloButton.java | 169 ++ .../common/button/ApolloButtonShape.java | 35 + .../common/button/ApolloButtonSize.java | 86 + .../common/button/ApolloButtonTooltip.java | 151 ++ .../button/action/ApolloButtonAction.java | 81 + .../action/ApolloButtonClientAction.java | 49 + .../common/button/action/ClientAction.java | 49 + .../common/button/action/OpenUrlAction.java | 47 + .../button/action/RunCommandAction.java | 47 + .../button/content/ApolloButtonContent.java | 210 +++ .../content/ApolloButtonContentPart.java | 102 ++ .../common/button/content/ComponentPart.java | 48 + .../common/button/content/IconPart.java | 51 + .../button/content/LiveComponentPart.java | 66 + .../apollo/common/icon/ItemStackIcon.java | 8 + .../apollo/common/location/HudPosition.java | 12 + .../ApolloPlayerInventoryCloseEvent.java | 64 + .../ApolloPlayerInventoryOpenEvent.java | 64 + .../apollo/module/chat/ChatButton.java | 123 ++ .../apollo/module/chat/ChatModule.java | 205 +++ .../module/inventory/InventoryButton.java | 148 ++ .../module/inventory/InventoryButtonBox.java | 35 + .../module/inventory/InventoryModule.java | 323 +++- .../module/inventory/InventoryType.java | 34 + .../PacketEnrichmentModule.java | 46 + .../module/button/ApolloButtonSerializer.java | 273 ++++ .../module/button/ButtonModuleSupport.java | 504 ++++++ .../apollo/module/button/ButtonSurface.java | 105 ++ .../module/chat/ChatButtonSerializer.java | 57 + .../apollo/module/chat/ChatModuleImpl.java | 170 +- .../inventory/InventoryButtonSerializer.java | 66 + .../module/inventory/InventoryModuleImpl.java | 221 +++ .../PacketEnrichmentImpl.java | 34 + .../apollo/network/ButtonNetworkTypes.java | 262 +++ .../apollo/network/NetworkTypes.java | 8 + .../option/config/CommonSerializers.java | 208 +++ docs/developers/events.mdx | 30 + .../lightweight/json/serverbound-packets.mdx | 24 + .../protobuf/serverbound-packets.mdx | 28 + docs/developers/modules/chat.mdx | 665 ++++++++ docs/developers/modules/inventory.mdx | 1430 +++++++++++++++++ docs/developers/modules/packetenrichment.mdx | 28 + docs/developers/utilities/_meta.json | 1 + docs/developers/utilities/buttons.mdx | 306 ++++ docs/developers/utilities/icons.mdx | 12 +- docs/public/modules/chat/channels.png | Bin 0 -> 5752 bytes docs/public/modules/chat/staff-chat.png | Bin 0 -> 9462 bytes docs/public/modules/inventory/hub.png | Bin 0 -> 40783 bytes docs/public/modules/inventory/menu.png | Bin 0 -> 39266 bytes docs/public/modules/inventory/minigame.png | Bin 0 -> 38504 bytes docs/public/modules/inventory/staff.png | Bin 0 -> 32947 bytes .../example/api/ApolloApiExamplePlatform.java | 2 + .../example/api/module/ChatApiExample.java | 43 + .../api/module/InventoryApiExample.java | 141 ++ .../module/chatbuttons/ChannelsLayout.java | 106 ++ .../module/chatbuttons/StaffChatLayout.java | 143 ++ .../module/inventorybuttons/HubLayout.java | 217 +++ .../module/inventorybuttons/MenuLayout.java | 282 ++++ .../inventorybuttons/MinigameLayout.java | 244 +++ .../module/inventorybuttons/StaffLayout.java | 327 ++++ .../apollo/example/ApolloExamplePlugin.java | 1 - .../apollo/example/command/ChatCommand.java | 44 +- .../example/command/InventoryCommand.java | 78 +- .../example/module/impl/ChatExample.java | 11 + .../example/module/impl/InventoryExample.java | 16 +- .../apollo/example/util/ServerStatsUtil.java | 77 + .../json/ApolloJsonExamplePlatform.java | 2 + .../ApolloPacketReceiveJsonListener.java | 26 + .../example/json/module/ChatJsonExample.java | 47 + .../json/module/InventoryJsonExample.java | 192 +++ .../module/chatbuttons/ChannelsLayout.java | 72 + .../module/chatbuttons/ChatButtonParts.java | 121 ++ .../module/chatbuttons/StaffChatLayout.java | 93 ++ .../module/inventorybuttons/HubLayout.java | 142 ++ .../InventoryButtonParts.java | 137 ++ .../module/inventorybuttons/MenuLayout.java | 187 +++ .../inventorybuttons/MinigameLayout.java | 142 ++ .../module/inventorybuttons/StaffLayout.java | 255 +++ .../example/json/util/JsonPacketUtil.java | 2 + .../proto/ApolloProtoExamplePlatform.java | 2 + .../ApolloPacketReceiveProtoListener.java | 32 + .../proto/module/ChatProtoExample.java | 56 + .../proto/module/InventoryProtoExample.java | 200 +++ .../module/chatbuttons/ChannelsLayout.java | 126 ++ .../module/chatbuttons/ChatButtonParts.java | 52 + .../module/chatbuttons/StaffChatLayout.java | 174 ++ .../module/inventorybuttons/HubLayout.java | 247 +++ .../InventoryButtonParts.java | 70 + .../module/inventorybuttons/MenuLayout.java | 338 ++++ .../inventorybuttons/MinigameLayout.java | 251 +++ .../module/inventorybuttons/StaffLayout.java | 398 +++++ .../proto/util/ProtobufPacketUtil.java | 2 + gradle/libs.versions.toml | 2 +- .../apollo/ApolloBukkitPlatform.java | 17 +- .../apollo/ApolloBungeePlatform.java | 17 + .../apollo/ApolloFoliaPlatform.java | 17 + .../apollo/ApolloMinestomPlatform.java | 22 +- .../apollo/ApolloVelocityPlatform.java | 20 + 99 files changed, 11894 insertions(+), 15 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/ApolloButton.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonShape.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonSize.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonTooltip.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonAction.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonClientAction.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/action/ClientAction.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/action/OpenUrlAction.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/action/RunCommandAction.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContent.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContentPart.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/content/ComponentPart.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/content/IconPart.java create mode 100644 api/src/main/java/com/lunarclient/apollo/common/button/content/LiveComponentPart.java create mode 100644 api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryCloseEvent.java create mode 100644 api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryOpenEvent.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/chat/ChatButton.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButton.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonBox.java create mode 100644 api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryType.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/button/ApolloButtonSerializer.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java create mode 100644 common/src/main/java/com/lunarclient/apollo/network/ButtonNetworkTypes.java create mode 100644 docs/developers/utilities/buttons.mdx create mode 100644 docs/public/modules/chat/channels.png create mode 100644 docs/public/modules/chat/staff-chat.png create mode 100644 docs/public/modules/inventory/hub.png create mode 100644 docs/public/modules/inventory/menu.png create mode 100644 docs/public/modules/inventory/minigame.png create mode 100644 docs/public/modules/inventory/staff.png create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/ChannelsLayout.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/StaffChatLayout.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/HubLayout.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MenuLayout.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java create mode 100644 example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java create mode 100644 example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/ServerStatsUtil.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/InventoryJsonExample.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChannelsLayout.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChatButtonParts.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/StaffChatLayout.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/HubLayout.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/InventoryButtonParts.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MenuLayout.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MinigameLayout.java create mode 100644 example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/StaffLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/InventoryProtoExample.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChannelsLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChatButtonParts.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/StaffChatLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/HubLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/InventoryButtonParts.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MenuLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MinigameLayout.java create mode 100644 example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/StaffLayout.java diff --git a/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java b/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java index f63d6cf4..4679d873 100644 --- a/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java +++ b/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java @@ -25,6 +25,7 @@ import com.lunarclient.apollo.option.Options; import com.lunarclient.apollo.stats.ApolloStats; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.jetbrains.annotations.ApiStatus; @@ -92,6 +93,14 @@ public interface ApolloPlatform { */ Object getPlugin(); + /** + * Returns the platform {@link Scheduler}. + * + * @return the platform scheduler + * @since 1.2.9 + */ + Scheduler getScheduler(); + /** * Represents the kind of server a platform is. * @@ -115,4 +124,26 @@ enum Platform { VELOCITY } + /** + * Represents the platform scheduler, running tasks through the + * platform plugin's own scheduler. + * + * @since 1.2.9 + */ + interface Scheduler { + + /** + * Schedules a task to run asynchronously every period, first running + * after the given delay. + * + * @param task the task to run + * @param delay the delay before the first run + * @param period the period between runs + * @param unit the unit of the delay and period + * @since 1.2.9 + */ + void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit); + + } + } diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButton.java b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButton.java new file mode 100644 index 00000000..3dbe830e --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButton.java @@ -0,0 +1,169 @@ +/* + * 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.button; + +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.action.ApolloButtonClientAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.location.HudPosition; +import java.awt.Color; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a button which can be shown on the client. + * + * @since 1.2.9 + */ +@Getter +@SuperBuilder(toBuilder = true) +@ApiStatus.NonExtendable +public abstract class ApolloButton { + + /** + * Returns the button {@link String} id. + * + *

Displaying another button with the same id replaces the + * previous one.

+ * + * @return the button id + * @since 1.2.9 + */ + @NotNull String id; + + /** + * Returns the {@link HudPosition} of this button, relative to the + * top-left corner of the container it is placed in. + * + *

The button must fit inside the container: {@code 0 <= x}, + * {@code 0 <= y}, {@code x + width <= container width} and + * {@code y + height <= container height}. See the surface type for + * its container dimensions (e.g. {@code InventoryButton#BOX_WIDTH}).

+ * + * @return the button position + * @since 1.2.9 + */ + @NotNull HudPosition position; + + /** + * Returns the {@link ApolloButtonSize} of this button. + * + *

Use one of the surface's suggested sizes (e.g. + * {@code InventoryButton.SIZE_MEDIUM}) or a fully custom + * {@link ApolloButtonSize#of(float, float)}.

+ * + * @return the button size + * @since 1.2.9 + */ + @NotNull ApolloButtonSize size; + + /** + * Returns the {@link ApolloButtonShape} of this button. + * + * @return the button shape + * @since 1.2.9 + */ + @NotNull ApolloButtonShape shape; + + /** + * Returns the {@link ApolloButtonContent} rendered inside this button. + * + *

Built via {@code ApolloButtonContent.builder()}, appending static + * components, icons or live per-player parts that are re-resolved + * whenever the content is sent (see the owning module's live button + * broadcast option, e.g. {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).

+ * + * @return the button content + * @since 1.2.9 + */ + @NotNull ApolloButtonContent content; + + /** + * Returns the {@link ApolloButtonTooltip} shown while this button is hovered. + * + *

Built via {@code ApolloButtonTooltip.of(...)} for static lines, + * or {@code ApolloButtonTooltip.live(...)} for per-player lines that + * are re-resolved whenever the button content is sent (see the owning + * module's live button broadcast option, e.g. + * {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).

+ * + * @return the tooltip, or {@code null} for no tooltip + * @since 1.2.9 + */ + @Builder.Default + @Nullable ApolloButtonTooltip tooltip = null; + + /** + * Returns the {@link ApolloButtonAction} executed when this button + * is clicked. + * + *

Built via {@link ApolloButtonAction#runCommand(String)}, + * {@link ApolloButtonAction#openUrl(String)} or + * {@link ApolloButtonAction#clientAction(ApolloButtonClientAction)}.

+ * + * @return the click action, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable ApolloButtonAction onClick = null; + + /** + * Returns the background {@link Color} used while this button is hovered. + * + * @return the hovered background color, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable Color hoveredBackgroundColor = null; + + /** + * Returns the border {@link Color} used while this button is hovered. + * + * @return the hovered border color, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable Color hoveredBorderColor = null; + + /** + * Returns the background {@link Color} of this button. + * + * @return the background color + * @since 1.2.9 + */ + public abstract Color getBackgroundColor(); + + /** + * Returns the border {@link Color} of this button. + * + * @return the border color + * @since 1.2.9 + */ + public abstract Color getBorderColor(); + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonShape.java b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonShape.java new file mode 100644 index 00000000..6ba14b6a --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonShape.java @@ -0,0 +1,35 @@ +/* + * 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.button; + +/** + * Represents the shape of a button. + * + * @since 1.2.9 + */ +public enum ApolloButtonShape { + ROUNDED_SQUARE, + CIRCLE + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonSize.java b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonSize.java new file mode 100644 index 00000000..6ed3d954 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonSize.java @@ -0,0 +1,86 @@ +/* + * 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.button; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents the size of an {@link ApolloButton}, in GUI-scaled pixels. + * + *

Surface types ship suggested sizes tuned to their container + * dimensions (e.g. {@code InventoryButton.SIZE_MEDIUM}); use + * {@link #of(float, float)} for a fully custom size.

+ * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class ApolloButtonSize { + + /** + * Creates a new {@link ApolloButtonSize} with the given dimensions. + * + * @param width the button width, must be finite and greater than 0 + * @param height the button height, must be finite and greater than 0 + * @return the button size + * @since 1.2.9 + */ + public static ApolloButtonSize of(float width, float height) { + if (!(width > 0.0F) || !(height > 0.0F) || !Float.isFinite(width) || !Float.isFinite(height)) { + throw new IllegalArgumentException("ApolloButtonSize dimensions must be finite and greater than 0"); + } + + return new ApolloButtonSize(width, height); + } + + /** + * Creates a new square {@link ApolloButtonSize}. + * + * @param size the button width and height, must be finite and greater than 0 + * @return the button size + * @since 1.2.9 + */ + public static ApolloButtonSize of(float size) { + return of(size, size); + } + + /** + * Returns the {@code float} button width. + * + * @return the button width + * @since 1.2.9 + */ + private final float width; + + /** + * Returns the {@code float} button height. + * + * @return the button height + * @since 1.2.9 + */ + private final float height; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonTooltip.java b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonTooltip.java new file mode 100644 index 00000000..4f5fb3a3 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/ApolloButtonTooltip.java @@ -0,0 +1,151 @@ +/* + * 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.button; + +import com.lunarclient.apollo.player.ApolloPlayer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Represents the tooltip of an {@link ApolloButton}, either a static + * list of lines or a live per-player resolver. + * + * @since 1.2.9 + */ +@Getter +public final class ApolloButtonTooltip { + + /** + * The maximum number of tooltip lines. + * + * @since 1.2.9 + */ + public static final int MAX_LINES = 100; + + /** + * Creates a static tooltip from the given lines. + * + * @param lines the tooltip lines + * @return the tooltip + * @since 1.2.9 + */ + public static ApolloButtonTooltip of(@NonNull Component... lines) { + return of(Arrays.asList(lines)); + } + + /** + * Creates a static tooltip from the given lines. + * + * @param lines the tooltip lines + * @return the tooltip + * @since 1.2.9 + */ + public static ApolloButtonTooltip of(@NonNull List lines) { + if (lines.size() > MAX_LINES) { + throw new IllegalArgumentException("ApolloButtonTooltip supports at most " + MAX_LINES + " lines"); + } + + for (Component line : lines) { + if (line == null) { + throw new IllegalArgumentException("ApolloButtonTooltip lines must not contain null"); + } + } + + return new ApolloButtonTooltip(lines, null, null); + } + + /** + * Creates a live tooltip, resolved into lines for each viewing + * {@link ApolloPlayer} and refreshed at the given interval. + * + *

The interval must be positive and is quantized to server ticks + * (50ms); anything below one tick refreshes every tick.

+ * + * @param resolver the per-player tooltip resolver + * @param updateInterval the refresh interval + * @return the tooltip + * @since 1.2.9 + */ + public static ApolloButtonTooltip live(@NonNull Function> resolver, + @NonNull Duration updateInterval) { + if (updateInterval.isNegative() || updateInterval.isZero()) { + throw new IllegalArgumentException("ApolloButtonTooltip#updateInterval must be positive"); + } + + return new ApolloButtonTooltip(null, resolver, updateInterval); + } + + /** + * Returns the static tooltip lines, or {@code null} if this tooltip is live. + * + * @return the tooltip lines, or {@code null} + * @since 1.2.9 + */ + private final @Nullable List lines; + + /** + * Returns the per-player live tooltip resolver, or {@code null} if this + * tooltip is static. + * + * @return the live tooltip resolver, or {@code null} + * @since 1.2.9 + */ + private final @Nullable Function> resolver; + + /** + * Returns the refresh interval of a live tooltip, or {@code null} if + * this tooltip is static. + * + * @return the update interval, or {@code null} + * @since 1.2.9 + */ + private final @Nullable Duration updateInterval; + + private ApolloButtonTooltip(@Nullable List lines, + @Nullable Function> resolver, + @Nullable Duration updateInterval) { + this.lines = lines == null ? null : Collections.unmodifiableList(new ArrayList<>(lines)); + this.resolver = resolver; + this.updateInterval = updateInterval; + } + + /** + * Returns whether this tooltip is live. + * + * @return whether this tooltip is live + * @since 1.2.9 + */ + public boolean isLive() { + return this.resolver != null; + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonAction.java b/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonAction.java new file mode 100644 index 00000000..1ec44ac0 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonAction.java @@ -0,0 +1,81 @@ +/* + * 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.button.action; + +import com.lunarclient.apollo.common.button.ApolloButton; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.jetbrains.annotations.ApiStatus; + +/** + * The abstract base class for the action executed when an + * {@link ApolloButton} is clicked. + * + *

Each action is one of {@link RunCommandAction}, {@link OpenUrlAction} + * or {@link ClientAction}.

+ * + * @since 1.2.9 + */ +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@ApiStatus.NonExtendable +public abstract class ApolloButtonAction { + + /** + * Creates an action that runs the given command as the player. + * + *

Must start with {@code /}.

+ * + * @param command the command to run + * @return the run command action + * @since 1.2.9 + */ + public static RunCommandAction runCommand(@NonNull String command) { + return new RunCommandAction(command); + } + + /** + * Creates an action that opens the given URL. + * + * @param url the url to open + * @return the open url action + * @since 1.2.9 + */ + public static OpenUrlAction openUrl(@NonNull String url) { + return new OpenUrlAction(url); + } + + /** + * Creates an action that executes a built-in + * {@link ApolloButtonClientAction}. + * + * @param action the client action to execute + * @return the client action + * @since 1.2.9 + */ + public static ClientAction clientAction(@NonNull ApolloButtonClientAction action) { + return new ClientAction(action); + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonClientAction.java b/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonClientAction.java new file mode 100644 index 00000000..76d8c57c --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/action/ApolloButtonClientAction.java @@ -0,0 +1,49 @@ +/* + * 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.button.action; + +/** + * Represents a built-in client action executed when a button is clicked. + * + * @since 1.2.9 + */ +public enum ApolloButtonClientAction { + + /** + * Opens the fullscreen minimap view. + * + *

Does nothing when the player has the MiniMap mod disabled.

+ * + * @since 1.2.9 + */ + OPEN_MINIMAP_VIEW, + + /** + * Opens the waypoints menu. + * + * @since 1.2.9 + */ + OPEN_WAYPOINTS_MENU + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/action/ClientAction.java b/api/src/main/java/com/lunarclient/apollo/common/button/action/ClientAction.java new file mode 100644 index 00000000..c9049773 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/action/ClientAction.java @@ -0,0 +1,49 @@ +/* + * 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.button.action; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents a button action that executes a built-in + * {@link ApolloButtonClientAction}. + * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class ClientAction extends ApolloButtonAction { + + /** + * Returns the built-in client action executed when the button + * is clicked. + * + * @return the client action + * @since 1.2.9 + */ + private final ApolloButtonClientAction action; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/action/OpenUrlAction.java b/api/src/main/java/com/lunarclient/apollo/common/button/action/OpenUrlAction.java new file mode 100644 index 00000000..d28e1d83 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/action/OpenUrlAction.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.common.button.action; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents a button action that opens a URL. + * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class OpenUrlAction extends ApolloButtonAction { + + /** + * Returns the URL opened when the button is clicked. + * + * @return the url + * @since 1.2.9 + */ + private final String url; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/action/RunCommandAction.java b/api/src/main/java/com/lunarclient/apollo/common/button/action/RunCommandAction.java new file mode 100644 index 00000000..65d4f709 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/action/RunCommandAction.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.common.button.action; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents a button action that runs a command as the player. + * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class RunCommandAction extends ApolloButtonAction { + + /** + * Returns the command run as the player when the button is clicked. + * + * @return the command + * @since 1.2.9 + */ + private final String command; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContent.java b/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContent.java new file mode 100644 index 00000000..edf468d8 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContent.java @@ -0,0 +1,210 @@ +/* + * 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.button.content; + +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.icon.Icon; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import lombok.Getter; +import lombok.NonNull; +import net.kyori.adventure.text.Component; + +/** + * Represents the content of an {@link ApolloButton}. + * + * @since 1.2.9 + */ +@Getter +public final class ApolloButtonContent { + + /** + * The maximum number of content parts per button. + * + * @since 1.2.9 + */ + public static final int MAX_PARTS = 30; + + /** + * The minimum content {@link #scale}. + * + * @since 1.2.9 + */ + public static final float MIN_SCALE = 0.25F; + + /** + * The maximum content {@link #scale}. + * + * @since 1.2.9 + */ + public static final float MAX_SCALE = 4.0F; + + /** + * Creates a new {@link Builder} for building button content. + * + * @return the content builder + * @since 1.2.9 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns an unmodifiable {@link List} of the content + * {@link ApolloButtonContentPart}s, in render order. + * + * @return the content parts + * @since 1.2.9 + */ + private final List parts; + + /** + * Returns the {@code float} scale factor applied to the content row. + * + * @return the content scale + * @since 1.2.9 + */ + private final float scale; + + /** + * Returns whether any part of this content is live. + * + * @return whether this content has live parts + * @since 1.2.9 + */ + private final boolean live; + + private ApolloButtonContent(List parts, float scale, boolean live) { + this.parts = Collections.unmodifiableList(parts); + this.scale = scale; + this.live = live; + } + + /** + * Represents a builder for {@link ApolloButtonContent}. + * + * @since 1.2.9 + */ + public static final class Builder { + + private final List parts = new ArrayList<>(); + private float scale = 1.0F; + private boolean live; + + private Builder() { + } + + /** + * Appends an adventure {@link Component} to the content. + * + * @param component the component to append + * @return this builder + * @since 1.2.9 + */ + public Builder append(@NonNull Component component) { + return this.append(ApolloButtonContentPart.component(component)); + } + + /** + * Appends an {@link Icon} to the content; see {@link IconPart}. + * + * @param icon the icon to append + * @return this builder + * @since 1.2.9 + */ + public Builder append(@NonNull Icon icon) { + return this.append(ApolloButtonContentPart.icon(icon)); + } + + /** + * Appends a live part, resolved into an adventure {@link Component} + * for each viewing {@link ApolloPlayer} and refreshed at the given + * interval. + * + * @param liveComponent the per-player component resolver to append + * @param updateInterval the refresh interval + * @return this builder + * @since 1.2.9 + */ + public Builder append(@NonNull Function liveComponent, + @NonNull Duration updateInterval) { + return this.append(ApolloButtonContentPart.live(liveComponent, updateInterval)); + } + + /** + * Appends a pre-built content part; see the + * {@link ApolloButtonContentPart} factories. + * + * @param part the content part to append + * @return this builder + * @since 1.2.9 + */ + public Builder append(@NonNull ApolloButtonContentPart part) { + this.parts.add(part); + this.live |= part.isLive(); + return this; + } + + /** + * Sets the {@code float} scale factor applied to the content row. + * Defaults to {@code 1.0}. + * + * @param scale the content scale, between {@link #MIN_SCALE} and {@link #MAX_SCALE} + * @return this builder + * @since 1.2.9 + */ + public Builder scale(float scale) { + if (!(scale >= MIN_SCALE && scale <= MAX_SCALE)) { + throw new IllegalArgumentException("ApolloButtonContent#scale must be between " + MIN_SCALE + " and " + MAX_SCALE); + } + + this.scale = scale; + return this; + } + + /** + * Builds the {@link ApolloButtonContent}. + * + * @return the built content + * @since 1.2.9 + */ + public ApolloButtonContent build() { + if (this.parts.isEmpty()) { + throw new IllegalArgumentException("ApolloButtonContent requires at least one part"); + } + + if (this.parts.size() > MAX_PARTS) { + throw new IllegalArgumentException("ApolloButtonContent supports at most " + MAX_PARTS + " parts"); + } + + return new ApolloButtonContent(new ArrayList<>(this.parts), this.scale, this.live); + } + + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContentPart.java b/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContentPart.java new file mode 100644 index 00000000..d9181a2b --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/content/ApolloButtonContentPart.java @@ -0,0 +1,102 @@ +/* + * 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.button.content; + +import com.lunarclient.apollo.common.icon.Icon; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.time.Duration; +import java.util.function.Function; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.ApiStatus; + +/** + * The abstract base class for a single {@link ApolloButtonContent} part. + * + *

Each part is one of {@link ComponentPart}, {@link IconPart} or + * {@link LiveComponentPart}, created through the factory methods on this + * class or the {@link ApolloButtonContent.Builder} append overloads.

+ * + * @since 1.2.9 + */ +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@ApiStatus.NonExtendable +public abstract class ApolloButtonContentPart { + + /** + * Creates a static text part from the given adventure {@link Component}. + * + * @param component the component to render + * @return the component part + * @since 1.2.9 + */ + public static ComponentPart component(@NonNull Component component) { + return new ComponentPart(component); + } + + /** + * Creates an icon part from the given {@link Icon}. + * + * @param icon the icon to render + * @return the icon part + * @since 1.2.9 + */ + public static IconPart icon(@NonNull Icon icon) { + return new IconPart(icon); + } + + /** + * Creates a live part, resolved into an adventure {@link Component} for + * each viewing {@link ApolloPlayer} and refreshed at the given interval. + * + *

The interval must be positive and is quantized to server ticks + * (50ms); anything below one tick refreshes every tick.

+ * + * @param resolver the per-player component resolver + * @param updateInterval the refresh interval + * @return the live component part + * @since 1.2.9 + */ + public static LiveComponentPart live(@NonNull Function resolver, + @NonNull Duration updateInterval) { + if (updateInterval.isNegative() || updateInterval.isZero()) { + throw new IllegalArgumentException("LiveComponentPart#updateInterval must be positive"); + } + + return new LiveComponentPart(resolver, updateInterval); + } + + /** + * Returns whether this part is live. + * + * @return whether this part is live + * @since 1.2.9 + */ + public boolean isLive() { + return false; + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/content/ComponentPart.java b/api/src/main/java/com/lunarclient/apollo/common/button/content/ComponentPart.java new file mode 100644 index 00000000..e2955e22 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/content/ComponentPart.java @@ -0,0 +1,48 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.common.button.content; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.Component; + +/** + * Represents a static text content part. + * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class ComponentPart extends ApolloButtonContentPart { + + /** + * Returns the adventure {@link Component} rendered by this part. + * + * @return the component + * @since 1.2.9 + */ + private final Component component; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/content/IconPart.java b/api/src/main/java/com/lunarclient/apollo/common/button/content/IconPart.java new file mode 100644 index 00000000..fbb4caca --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/content/IconPart.java @@ -0,0 +1,51 @@ +/* + * 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.button.content; + +import com.lunarclient.apollo.common.icon.Icon; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents an icon content part. + * + *

Can be any of the icons found in the {@link com.lunarclient.apollo.common.icon} package; + * for the most common use case, use {@link com.lunarclient.apollo.common.icon.ItemStackIcon}.

+ * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class IconPart extends ApolloButtonContentPart { + + /** + * Returns the {@link Icon} rendered by this part. + * + * @return the icon + * @since 1.2.9 + */ + private final Icon icon; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/common/button/content/LiveComponentPart.java b/api/src/main/java/com/lunarclient/apollo/common/button/content/LiveComponentPart.java new file mode 100644 index 00000000..faab4d7a --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/common/button/content/LiveComponentPart.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.common.button.content; + +import com.lunarclient.apollo.player.ApolloPlayer; +import java.time.Duration; +import java.util.function.Function; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.Component; + +/** + * Represents a live content part: a per-player {@link Component} resolver + * with an update interval controlling how often the module's live button + * broadcast re-resolves and re-sends it. + * + * @since 1.2.9 + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public final class LiveComponentPart extends ApolloButtonContentPart { + + /** + * Returns the per-player live {@link Component} resolver. + * + * @return the component resolver + * @since 1.2.9 + */ + private final Function resolver; + + /** + * Returns the refresh interval. + * + * @return the update interval + * @since 1.2.9 + */ + private final Duration updateInterval; + + @Override + public boolean isLive() { + return true; + } + +} 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 3913eee7..ff1fffe7 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 @@ -79,4 +79,12 @@ public final class ItemStackIcon extends Icon { */ @Nullable Profile profile; + /** + * Returns the icon {@link String} potion id (e.g. {@code "healing"}). + * + * @return the icon potion id + * @since 1.2.9 + */ + @Nullable String potion; + } diff --git a/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java b/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java index c6f7f4fa..4d3f7a49 100644 --- a/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java +++ b/api/src/main/java/com/lunarclient/apollo/common/location/HudPosition.java @@ -35,6 +35,18 @@ @Builder public final class HudPosition { + /** + * Creates a new {@link HudPosition} with the given coordinates. + * + * @param x the x coordinate + * @param y the y coordinate + * @return the hud position + * @since 1.2.9 + */ + public static HudPosition of(float x, float y) { + return new HudPosition(x, y); + } + /** * Returns the {@code float} X coordinate for this HUD position. * diff --git a/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryCloseEvent.java b/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryCloseEvent.java new file mode 100644 index 00000000..ab47b864 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryCloseEvent.java @@ -0,0 +1,64 @@ +/* + * 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.event.packetenrichment.inventory; + +import com.lunarclient.apollo.event.Event; +import com.lunarclient.apollo.module.packetenrichment.PlayerInfo; +import com.lunarclient.apollo.player.ApolloPlayer; +import lombok.Value; + +/** + * Represents an event that is fired when the player closes their inventory. + * + * @since 1.2.9 + */ +@Value +public class ApolloPlayerInventoryCloseEvent implements Event { + + /** + * The player that sent the packet. + * + * @return the player + * @since 1.2.9 + */ + ApolloPlayer player; + + /** + * The {@code long} representing the unix timestamp + * when the packet was created. + * + * @return the unix timestamp + * @since 1.2.9 + */ + long instantiationTimeMs; + + /** + * The player's {@link PlayerInfo} information. + * + * @return the player's player info + * @since 1.2.9 + */ + PlayerInfo playerInfo; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryOpenEvent.java b/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryOpenEvent.java new file mode 100644 index 00000000..9d0c3dde --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/event/packetenrichment/inventory/ApolloPlayerInventoryOpenEvent.java @@ -0,0 +1,64 @@ +/* + * 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.event.packetenrichment.inventory; + +import com.lunarclient.apollo.event.Event; +import com.lunarclient.apollo.module.packetenrichment.PlayerInfo; +import com.lunarclient.apollo.player.ApolloPlayer; +import lombok.Value; + +/** + * Represents an event that is fired when the player opens their inventory. + * + * @since 1.2.9 + */ +@Value +public class ApolloPlayerInventoryOpenEvent implements Event { + + /** + * The player that sent the packet. + * + * @return the player + * @since 1.2.9 + */ + ApolloPlayer player; + + /** + * The {@code long} representing the unix timestamp + * when the packet was created. + * + * @return the unix timestamp + * @since 1.2.9 + */ + long instantiationTimeMs; + + /** + * The player's {@link PlayerInfo} information. + * + * @return the player's player info + * @since 1.2.9 + */ + PlayerInfo playerInfo; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/chat/ChatButton.java b/api/src/main/java/com/lunarclient/apollo/module/chat/ChatButton.java new file mode 100644 index 00000000..441bbd93 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/chat/ChatButton.java @@ -0,0 +1,123 @@ +/* + * 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.chat; + +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import java.awt.Color; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a chat button which can be shown on the client, placed in a + * single fixed-size box anchored in the strip between the chat input field + * and the chat log, visible while the chat screen is open. + * + * @since 1.2.9 + */ +@Getter +@SuperBuilder(toBuilder = true) +public final class ChatButton extends ApolloButton { + + /** + * The width of the button box. + * + * @since 1.2.9 + */ + public static final float BOX_WIDTH = 320.0F; + + /** + * The height of the button box. + * + * @since 1.2.9 + */ + public static final float BOX_HEIGHT = 20.0F; + + /** + * The maximum number of buttons in the box. + * + * @since 1.2.9 + */ + public static final int MAX_BUTTONS = 25; + + /** + * A suggested small size (56x16); five fit side by side in the + * button box. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_SMALL = ApolloButtonSize.of(56.0F, 16.0F); + + /** + * A suggested medium size (96x16); three fit side by side in the + * button box. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_MEDIUM = ApolloButtonSize.of(96.0F, 16.0F); + + /** + * A suggested icon size (16x16) for square icon-only buttons. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_ICON = ApolloButtonSize.of(16.0F); + + /** + * The suggested default background {@link Color} of a chat button, + * matching the vanilla chat background fill. + * + * @since 1.2.9 + */ + public static final Color DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 0, 128); + + /** + * The suggested default border {@link Color} of a chat button, matching + * the background so buttons read as part of the chat. + * + * @since 1.2.9 + */ + public static final Color DEFAULT_BORDER_COLOR = new Color(0, 0, 0, 128); + + /** + * Returns the background {@link Color} of this button. + * + * @return the background color + * @since 1.2.9 + */ + @Builder.Default + @NotNull Color backgroundColor = DEFAULT_BACKGROUND_COLOR; + + /** + * Returns the border {@link Color} of this button. + * + * @return the border color + * @since 1.2.9 + */ + @Builder.Default + @NotNull Color borderColor = DEFAULT_BORDER_COLOR; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/chat/ChatModule.java b/api/src/main/java/com/lunarclient/apollo/module/chat/ChatModule.java index c6190500..2c4f41dd 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/chat/ChatModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/chat/ChatModule.java @@ -23,11 +23,28 @@ */ package com.lunarclient.apollo.module.chat; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; import com.lunarclient.apollo.module.ApolloModule; import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.option.ListOption; +import com.lunarclient.apollo.option.Option; +import com.lunarclient.apollo.option.SimpleOption; import com.lunarclient.apollo.recipients.Recipients; +import io.leangen.geantyref.TypeToken; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; /** * Represents the chat module. @@ -38,6 +55,68 @@ @ModuleDefinition(id = "chat", name = "Chat") public abstract class ChatModule extends ApolloModule { + /** + * Controls whether live {@link ChatButton} content is automatically + * re-resolved and re-sent to viewers. + * + *

When enabled without further setup, updates are always sent to + * every player holding live buttons, even while their chat is closed. + * Also enable the packet enrichment module and its player chat + * open/close packets and events to only send updates to players who + * currently have their chat open.

+ * + * @since 1.2.9 + */ + public static final SimpleOption BROADCAST_LIVE_BUTTONS = Option.builder() + .comment( + "Set to 'true' to automatically re-send resolved live chat button content, otherwise 'false'.", + "When enabled, updates are always sent, even to players whose chat is closed; also enable", + "the packet enrichment module and its player chat open/close packets and events to only", + "send updates to players who currently have their chat open." + ) + .node("buttons", "live-broadcast").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + /** + * Controls whether the {@link #DEFAULT_BUTTONS} are displayed to + * players when they join. + * + * @since 1.2.9 + */ + public static final SimpleOption SEND_DEFAULT_BUTTONS = Option.builder() + .comment("Set to 'true' to display the default buttons to players when they join, otherwise 'false'.") + .node("buttons", "send-defaults").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + /** + * The default {@link ChatButton}s displayed to joining players while + * {@link #SEND_DEFAULT_BUTTONS} is enabled. + * + * @since 1.2.9 + */ + public static final ListOption DEFAULT_BUTTONS = Option.list() + .comment( + "Sets the default buttons to display to players when they join, while send-defaults is enabled.", + "Text is read as legacy strings ('&'-color codes) and icons as icon definitions; live values", + "are only available through the API." + ) + .node("buttons", "defaults").type(new TypeToken>() {}) + .defaultValue(ChatModule.createDefaultButtons()) + .build(); + + protected ChatModule() { + this.registerOptions( + ChatModule.BROADCAST_LIVE_BUTTONS, + ChatModule.SEND_DEFAULT_BUTTONS, + ChatModule.DEFAULT_BUTTONS + ); + } + + @Override + public boolean isClientNotify() { + return true; + } + /** * Displays the message to the {@link Recipients}. * @@ -57,4 +136,130 @@ public abstract class ChatModule extends ApolloModule { */ public abstract void removeLiveChatMessage(Recipients recipients, int messageId); + /** + * Displays the {@link ChatButton}s to the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttons the chat buttons + * @since 1.2.9 + */ + public abstract void displayChatButtons(Recipients recipients, Collection buttons); + + /** + * Displays the {@link ChatButton} to the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param button the chat button + * @since 1.2.9 + */ + public abstract void displayChatButton(Recipients recipients, ChatButton button); + + /** + * Removes the {@link ChatButton} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the chat button id + * @since 1.2.9 + */ + public abstract void removeChatButton(Recipients recipients, String buttonId); + + /** + * Removes the {@link ChatButton} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param button the chat button + * @since 1.2.9 + */ + public abstract void removeChatButton(Recipients recipients, ChatButton button); + + /** + * Resets all {@link ChatButton}s for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.9 + */ + public abstract void resetChatButtons(Recipients recipients); + + /** + * Updates the content and tooltip of a previously displayed + * {@link ChatButton} for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the chat button id + * @param content the new button content + * @param tooltip the new tooltip, or {@code null} to clear the tooltip + * @since 1.2.9 + */ + public abstract void updateChatButton(Recipients recipients, String buttonId, + ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip); + + /** + * Updates only the content of a previously displayed + * {@link ChatButton} for the {@link Recipients}, keeping the + * previous tooltip. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the chat button id + * @param content the new button content + * @since 1.2.9 + */ + public abstract void updateChatButtonContent(Recipients recipients, String buttonId, + ApolloButtonContent content); + + /** + * Updates only the tooltip of a previously displayed + * {@link ChatButton} for the {@link Recipients}, keeping the + * previous content. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the chat button id + * @param tooltip the new tooltip, or {@code null} to clear the tooltip + * @since 1.2.9 + */ + public abstract void updateChatButtonTooltip(Recipients recipients, String buttonId, + @Nullable ApolloButtonTooltip tooltip); + + private static List createDefaultButtons() { + ChatButton teamChat = ChatButton.builder() + .id("team-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("SHIELD").build()) + .append(Component.text("Team Chat", NamedTextColor.GREEN)) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel team")) + .build(); + + ChatButton publicChat = ChatButton.builder() + .id("public-chat") + .position(HudPosition.of(76, 2)) + .size(ApolloButtonSize.of(78, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("OAK_SIGN").build()) + .append(Component.text("Public Chat")) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel public")) + .build(); + + ChatButton partyChat = ChatButton.builder() + .id("party-chat") + .position(HudPosition.of(160, 2)) + .size(ApolloButtonSize.of(76, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("FIREWORK_ROCKET").build()) + .append(Component.text("Party Chat", NamedTextColor.LIGHT_PURPLE)) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel party")) + .build(); + + return new ArrayList<>(Arrays.asList(teamChat, publicChat, partyChat)); + } + } diff --git a/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButton.java b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButton.java new file mode 100644 index 00000000..05da39f8 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButton.java @@ -0,0 +1,148 @@ +/* + * 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.inventory; + +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import java.awt.Color; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; + +/** + * Represents an inventory button which can be shown on the client, placed + * in one of the two boxes in the player inventory. + * + * @since 1.2.9 + */ +@Getter +@SuperBuilder(toBuilder = true) +public final class InventoryButton extends ApolloButton { + + /** + * The width of each button box. + * + * @since 1.2.9 + */ + public static final float BOX_WIDTH = 92.0F; + + /** + * The height of each button box. + * + * @since 1.2.9 + */ + public static final float BOX_HEIGHT = 166.0F; + + /** + * The maximum number of buttons per box. + * + * @since 1.2.9 + */ + public static final int MAX_BUTTONS_PER_BOX = 25; + + /** + * The suggested default background {@link Color} of an inventory button. + * + * @since 1.2.9 + */ + public static final Color DEFAULT_BACKGROUND_COLOR = new Color(255, 255, 255, 40); + + /** + * The suggested default border {@link Color} of an inventory button. + * + * @since 1.2.9 + */ + public static final Color DEFAULT_BORDER_COLOR = new Color(37, 37, 37, 128); + + /** + * A suggested small square size (26x26); three fit side by side + * in a button box row. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_SMALL = ApolloButtonSize.of(26.0F); + + /** + * A suggested medium square size (40x40); two fit side by side + * in a button box row. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_MEDIUM = ApolloButtonSize.of(40.0F); + + /** + * A suggested large square size (84x84), spanning a full button + * box row. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_LARGE = ApolloButtonSize.of(84.0F); + + /** + * A suggested wide size (80x26), fitting text buttons spanning + * the box width. + * + * @since 1.2.9 + */ + public static final ApolloButtonSize SIZE_WIDE = ApolloButtonSize.of(80.0F, 26.0F); + + /** + * Returns the {@link InventoryType} this button belongs to. + * + * @return the inventory type + * @since 1.2.9 + */ + @NotNull InventoryType inventoryType; + + /** + * Returns the {@link InventoryButtonBox} this button is placed in. + * + *

Two fixed-size boxes ({@value #BOX_WIDTH}x{@value #BOX_HEIGHT}) + * show in the player inventory on its left and right side.

+ * + * @return the button box + * @since 1.2.9 + */ + @NotNull InventoryButtonBox box; + + /** + * Returns the background {@link Color} of this button. + * + * @return the background color + * @since 1.2.9 + */ + @Builder.Default + @NotNull Color backgroundColor = DEFAULT_BACKGROUND_COLOR; + + /** + * Returns the border {@link Color} of this button. + * + * @return the border color + * @since 1.2.9 + */ + @Builder.Default + @NotNull Color borderColor = DEFAULT_BORDER_COLOR; + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonBox.java b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonBox.java new file mode 100644 index 00000000..9d49341f --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonBox.java @@ -0,0 +1,35 @@ +/* + * 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.inventory; + +/** + * Represents the box an inventory button is placed in. + * + * @since 1.2.9 + */ +public enum InventoryButtonBox { + LEFT, + RIGHT + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModule.java b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModule.java index ed5861e3..450a7043 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModule.java @@ -23,9 +23,28 @@ */ package com.lunarclient.apollo.module.inventory; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; import com.lunarclient.apollo.module.ApolloModule; import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.option.ListOption; +import com.lunarclient.apollo.option.Option; +import com.lunarclient.apollo.option.SimpleOption; +import com.lunarclient.apollo.recipients.Recipients; +import io.leangen.geantyref.TypeToken; +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; /** * Represents the inventory module. @@ -34,10 +53,64 @@ */ @ApiStatus.NonExtendable @ModuleDefinition(id = "inventory", name = "Inventory") -public class InventoryModule extends ApolloModule { +public abstract class InventoryModule extends ApolloModule { + + /** + * Controls whether live {@link InventoryButton} content is automatically + * re-resolved and re-sent to viewers. + * + *

When enabled without further setup, updates are always sent to + * every player holding live buttons, even while their inventory is + * closed. Also enable the packet enrichment module and its player + * inventory open/close packets and events to only send updates to + * players who currently have their inventory open.

+ * + * @since 1.2.9 + */ + public static final SimpleOption BROADCAST_LIVE_BUTTONS = Option.builder() + .comment( + "Set to 'true' to automatically re-send resolved live inventory button content, otherwise 'false'.", + "When enabled, updates are always sent, even to players whose inventory is closed; also enable", + "the packet enrichment module and its player inventory open/close packets and events to only", + "send updates to players who currently have their inventory open." + ) + .node("buttons", "live-broadcast").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + /** + * Controls whether the {@link #DEFAULT_BUTTONS} are displayed to + * players when they join. + * + * @since 1.2.9 + */ + public static final SimpleOption SEND_DEFAULT_BUTTONS = Option.builder() + .comment("Set to 'true' to display the default buttons to players when they join, otherwise 'false'.") + .node("buttons", "send-defaults").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + /** + * The default {@link InventoryButton}s displayed to joining players + * while {@link #SEND_DEFAULT_BUTTONS} is enabled. + * + * @since 1.2.9 + */ + public static final ListOption DEFAULT_BUTTONS = Option.list() + .comment( + "Sets the default buttons to display to players when they join, while send-defaults is enabled.", + "Text is read as legacy strings ('&'-color codes) and icons as icon definitions; live values", + "are only available through the API." + ) + .node("buttons", "defaults").type(new TypeToken>() {}) + .defaultValue(InventoryModule.createDefaultButtons()) + .build(); protected InventoryModule() { - this.registerOptions(ApolloModule.ENABLE_OPTION_OFF); + this.registerOptions( + ApolloModule.ENABLE_OPTION_OFF, + InventoryModule.BROADCAST_LIVE_BUTTONS, + InventoryModule.SEND_DEFAULT_BUTTONS, + InventoryModule.DEFAULT_BUTTONS + ); } @Override @@ -45,4 +118,250 @@ public boolean isClientNotify() { return true; } + /** + * Displays the {@link InventoryButton}s to the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttons the inventory buttons + * @since 1.2.9 + */ + public abstract void displayInventoryButtons(Recipients recipients, Collection buttons); + + /** + * Displays the {@link InventoryButton} to the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param button the inventory button + * @since 1.2.9 + */ + public abstract void displayInventoryButton(Recipients recipients, InventoryButton button); + + /** + * Removes the {@link InventoryButton} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the inventory button id + * @since 1.2.9 + */ + public abstract void removeInventoryButton(Recipients recipients, String buttonId); + + /** + * Removes the {@link InventoryButton} from the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param button the inventory button + * @since 1.2.9 + */ + public abstract void removeInventoryButton(Recipients recipients, InventoryButton button); + + /** + * Resets all {@link InventoryButton}s for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.9 + */ + public abstract void resetInventoryButtons(Recipients recipients); + + /** + * Updates the content and tooltip of a previously displayed + * {@link InventoryButton} for the {@link Recipients}. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the inventory button id + * @param content the new button content + * @param tooltip the new tooltip, or {@code null} to clear the tooltip + * @since 1.2.9 + */ + public abstract void updateInventoryButton(Recipients recipients, String buttonId, + ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip); + + /** + * Updates only the content of a previously displayed + * {@link InventoryButton} for the {@link Recipients}, keeping the + * previous tooltip. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the inventory button id + * @param content the new button content + * @since 1.2.9 + */ + public abstract void updateInventoryButtonContent(Recipients recipients, String buttonId, + ApolloButtonContent content); + + /** + * Updates only the tooltip of a previously displayed + * {@link InventoryButton} for the {@link Recipients}, keeping the + * previous content. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the inventory button id + * @param tooltip the new tooltip, or {@code null} to clear the tooltip + * @since 1.2.9 + */ + public abstract void updateInventoryButtonTooltip(Recipients recipients, String buttonId, + @Nullable ApolloButtonTooltip tooltip); + + private static List createDefaultButtons() { + InventoryButton shop = InventoryButton.builder() + .id("shop") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("EMERALD").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse categories and buy items", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/shop")) + .build(); + + InventoryButton spawn = InventoryButton.builder() + .id("spawn") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("RED_BED").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Spawn", NamedTextColor.AQUA), + Component.text("Teleport back to spawn", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/spawn")) + .build(); + + InventoryButton warps = InventoryButton.builder() + .id("warps") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("COMPASS").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Warps", NamedTextColor.AQUA), + Component.text("Browse public warps", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/warps")) + .build(); + + InventoryButton enderChest = InventoryButton.builder() + .id("enderchest") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("ENDER_CHEST").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE), + Component.text("Open your personal storage", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/enderchest")) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("PLAYER_HEAD").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Your Profile", NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("COMPARATOR").build()) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + InventoryButton vote = InventoryButton.builder() + .id("vote") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(34, 204, 68, 64)) + .borderColor(new Color(190, 255, 205, 110)) + .hoveredBackgroundColor(new Color(34, 204, 68, 130)) + .hoveredBorderColor(new Color(190, 255, 205, 210)) + .content(ApolloButtonContent.builder() + .append(Component.text("Vote")) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://example.com/vote")) + .build(); + + InventoryButton discord = InventoryButton.builder() + .id("discord") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 84)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(88, 101, 242, 90)) + .borderColor(new Color(150, 160, 250, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Discord")) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Discord", NamedTextColor.BLUE), + Component.text("Join our community", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://lunarclient.dev/discord")) + .build(); + + InventoryButton lobby = InventoryButton.builder() + .id("lobby") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 136)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Back to Lobby")) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/lobby")) + .build(); + + return new ArrayList<>(Arrays.asList(shop, spawn, warps, enderChest, + profile, settings, vote, discord, lobby)); + } + } diff --git a/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryType.java b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryType.java new file mode 100644 index 00000000..d0e8a53f --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryType.java @@ -0,0 +1,34 @@ +/* + * 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.inventory; + +/** + * Represents the inventory screen an inventory button belongs to. + * + * @since 1.2.9 + */ +public enum InventoryType { + PLAYER + +} diff --git a/api/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentModule.java b/api/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentModule.java index cec82188..c1f6ee9b 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentModule.java +++ b/api/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentModule.java @@ -102,6 +102,48 @@ public abstract class PacketEnrichmentModule extends ApolloModule { .node("player-chat-close", "fire-apollo-event").type(TypeToken.get(Boolean.class)) .defaultValue(false).build(); + /** + * Controls whether the client sends an additional player inventory open packet to the server. + * + * @since 1.2.9 + */ + public static final SimpleOption PLAYER_INVENTORY_OPEN_PACKET = Option.builder() + .comment("Set to 'true' to have the client send an additional player inventory open packet to the server, otherwise 'false'.") + .node("player-inventory-open", "send-packet").type(TypeToken.get(Boolean.class)) + .defaultValue(false).notifyClient().build(); + + /** + * Controls whether Apollo fires {@link com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryOpenEvent} + * when the packet is received. + * + * @since 1.2.9 + */ + public static final SimpleOption PLAYER_INVENTORY_OPEN_EVENT = Option.builder() + .comment("If 'true', Apollo fires the player inventory open event on the main thread. Disable this and handle the packet yourself if you require asynchronous or off-thread processing.") + .node("player-inventory-open", "fire-apollo-event").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + /** + * Controls whether the client sends an additional player inventory close packet to the server. + * + * @since 1.2.9 + */ + public static final SimpleOption PLAYER_INVENTORY_CLOSE_PACKET = Option.builder() + .comment("Set to 'true' to have the client send an additional player inventory close packet to the server, otherwise 'false'.") + .node("player-inventory-close", "send-packet").type(TypeToken.get(Boolean.class)) + .defaultValue(false).notifyClient().build(); + + /** + * Controls whether Apollo fires {@link com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryCloseEvent} + * when the packet is received. + * + * @since 1.2.9 + */ + public static final SimpleOption PLAYER_INVENTORY_CLOSE_EVENT = Option.builder() + .comment("If 'true', Apollo fires the player inventory close event on the main thread. Disable this and handle the packet yourself if you require asynchronous or off-thread processing.") + .node("player-inventory-close", "fire-apollo-event").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + /** * Controls whether the client sends an additional player use item packet to the server. * @@ -153,6 +195,10 @@ protected PacketEnrichmentModule() { PacketEnrichmentModule.PLAYER_CHAT_OPEN_EVENT, PacketEnrichmentModule.PLAYER_CHAT_CLOSE_PACKET, PacketEnrichmentModule.PLAYER_CHAT_CLOSE_EVENT, + PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_PACKET, + PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_EVENT, + PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_PACKET, + PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_EVENT, PacketEnrichmentModule.PLAYER_USE_ITEM_PACKET, PacketEnrichmentModule.PLAYER_USE_ITEM_EVENT, PacketEnrichmentModule.PLAYER_USE_ITEM_BUCKET_PACKET, diff --git a/common/src/main/java/com/lunarclient/apollo/module/button/ApolloButtonSerializer.java b/common/src/main/java/com/lunarclient/apollo/module/button/ApolloButtonSerializer.java new file mode 100644 index 00000000..babf1248 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/button/ApolloButtonSerializer.java @@ -0,0 +1,273 @@ +/* + * 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.button; + +import com.lunarclient.apollo.common.ApolloComponent; +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.action.ApolloButtonClientAction; +import com.lunarclient.apollo.common.button.action.ClientAction; +import com.lunarclient.apollo.common.button.action.OpenUrlAction; +import com.lunarclient.apollo.common.button.action.RunCommandAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart; +import com.lunarclient.apollo.common.button.content.ComponentPart; +import com.lunarclient.apollo.common.button.content.IconPart; +import com.lunarclient.apollo.common.button.content.LiveComponentPart; +import com.lunarclient.apollo.common.icon.Icon; +import com.lunarclient.apollo.common.location.HudPosition; +import java.awt.Color; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +/** + * Reads and writes the shared {@link ApolloButton} properties from a module + * configuration; each button surface extends this with its own placement + * fields. + * + * @param the surface button type + * @since 1.2.9 + */ +public abstract class ApolloButtonSerializer implements TypeSerializer { + + @Override + public T deserialize(Type type, ConfigurationNode node) throws SerializationException { + try { + ApolloButtonContent.Builder content = ApolloButtonContent.builder(); + for (ConfigurationNode part : node.node("content", "parts").childrenList()) { + if (part.hasChild("text")) { + content.append(ApolloComponent.fromLegacyAmpersand(part.node("text").getString(""))); + } else if (part.hasChild("icon")) { + Icon icon = part.node("icon").get(Icon.class); + if (icon == null) { + throw new SerializationException("Button content part 'icon' must not be empty!"); + } + + content.append(icon); + } else { + throw new SerializationException("Button content parts require a 'text' or 'icon' field!"); + } + } + + if (node.hasChild("content", "scale")) { + content.scale((float) node.node("content", "scale").getDouble(1.0D)); + } + + String id = this.virtualNode(node, "id").getString(); + if (id == null || id.isEmpty()) { + throw new SerializationException("Required field id must not be empty!"); + } + + ApolloButton.ApolloButtonBuilder builder = this.createBuilder(node); + builder.id(id); + builder.position(HudPosition.of( + (float) this.virtualNode(node, "position", "x").getDouble(), + (float) this.virtualNode(node, "position", "y").getDouble())); + builder.size(ApolloButtonSize.of( + (float) this.virtualNode(node, "size", "width").getDouble(), + (float) this.virtualNode(node, "size", "height").getDouble())); + builder.shape(this.parseEnum(ApolloButtonShape.class, this.virtualNode(node, "shape").getString(), "shape")); + builder.content(content.build()); + builder.hoveredBackgroundColor(node.node("hovered-background-color").get(Color.class)); + builder.hoveredBorderColor(node.node("hovered-border-color").get(Color.class)); + + if (node.hasChild("tooltip")) { + List lines = new ArrayList<>(); + for (ConfigurationNode line : node.node("tooltip").childrenList()) { + lines.add(ApolloComponent.fromLegacyAmpersand(line.getString(""))); + } + + builder.tooltip(ApolloButtonTooltip.of(lines)); + } + + if (node.hasChild("on-click")) { + builder.onClick(this.readAction(node.node("on-click"))); + } + + return builder.build(); + } catch (IllegalArgumentException exception) { + throw new SerializationException(exception.getMessage()); + } + } + + @Override + public void serialize(Type type, @Nullable T button, ConfigurationNode node) throws SerializationException { + if (button == null) { + node.raw(null); + return; + } + + node.node("id").set(button.getId()); + this.serializeSurface(button, node); + node.node("position", "x").set((double) button.getPosition().getX()); + node.node("position", "y").set((double) button.getPosition().getY()); + node.node("size", "width").set((double) button.getSize().getWidth()); + node.node("size", "height").set((double) button.getSize().getHeight()); + node.node("shape").set(button.getShape().name()); + node.node("background-color").set(Color.class, button.getBackgroundColor()); + node.node("border-color").set(Color.class, button.getBorderColor()); + + if (button.getHoveredBackgroundColor() != null) { + node.node("hovered-background-color").set(Color.class, button.getHoveredBackgroundColor()); + } + + if (button.getHoveredBorderColor() != null) { + node.node("hovered-border-color").set(Color.class, button.getHoveredBorderColor()); + } + + node.node("content", "scale").set((double) button.getContent().getScale()); + for (ApolloButtonContentPart part : button.getContent().getParts()) { + this.writePart(node.node("content", "parts").appendListNode(), part); + } + + ApolloButtonTooltip tooltip = button.getTooltip(); + if (tooltip != null) { + if (tooltip.isLive() || tooltip.getLines() == null) { + throw new SerializationException("Live tooltips cannot be stored in the config!"); + } + + for (Component line : tooltip.getLines()) { + node.node("tooltip").appendListNode().set(ApolloComponent.toLegacyAmpersand(line)); + } + } + + ApolloButtonAction action = button.getOnClick(); + if (action instanceof RunCommandAction) { + node.node("on-click", "run-command").set(((RunCommandAction) action).getCommand()); + } else if (action instanceof OpenUrlAction) { + node.node("on-click", "open-url").set(((OpenUrlAction) action).getUrl()); + } else if (action instanceof ClientAction) { + node.node("on-click", "client-action").set(((ClientAction) action).getAction().name()); + } + } + + /** + * Creates the surface builder, reading the surface placement + * fields from the given node. + * + * @param node the button node + * @return the surface builder + * @throws SerializationException if a surface field is invalid + * @since 1.2.9 + */ + protected abstract ApolloButton.ApolloButtonBuilder createBuilder(ConfigurationNode node) throws SerializationException; + + /** + * Writes the surface placement fields to the given node. + * + * @param button the button to serialize + * @param node the button node + * @throws SerializationException if a surface field cannot be written + * @since 1.2.9 + */ + protected void serializeSurface(T button, ConfigurationNode node) throws SerializationException { + } + + /** + * Parses an enum constant. + * + * @param type the enum type + * @param value the config value + * @param field the field name used in error messages + * @param the enum type + * @return the parsed constant + * @throws SerializationException if the value is missing or unknown + * @since 1.2.9 + */ + protected > E parseEnum(Class type, @Nullable String value, String field) throws SerializationException { + if (value == null) { + throw new SerializationException("Required field " + field + " not found!"); + } + + try { + return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException exception) { + throw new SerializationException("Unknown " + field + " '" + value + "'!"); + } + } + + /** + * Returns the node at the given path, requiring it to exist. + * + * @param source the source node + * @param path the child path + * @return the child node + * @throws SerializationException if the child does not exist + * @since 1.2.9 + */ + protected ConfigurationNode virtualNode(ConfigurationNode source, Object... path) throws SerializationException { + if (!source.hasChild(path)) { + throw new SerializationException("Required field " + Arrays.toString(path) + " not found!"); + } + + return source.node(path); + } + + private ApolloButtonAction readAction(ConfigurationNode node) throws SerializationException { + if (node.hasChild("run-command")) { + return ApolloButtonAction.runCommand(node.node("run-command").getString("")); + } + + if (node.hasChild("open-url")) { + return ApolloButtonAction.openUrl(node.node("open-url").getString("")); + } + + if (node.hasChild("client-action")) { + return ApolloButtonAction.clientAction(this.parseEnum(ApolloButtonClientAction.class, + node.node("client-action").getString(), "client-action")); + } + + throw new SerializationException("on-click requires a 'run-command', 'open-url' or 'client-action' field!"); + } + + private void writePart(ConfigurationNode node, ApolloButtonContentPart part) throws SerializationException { + if (part instanceof LiveComponentPart) { + throw new SerializationException("Live content parts cannot be stored in the config!"); + } + + if (part instanceof ComponentPart) { + node.node("text").set(ApolloComponent.toLegacyAmpersand(((ComponentPart) part).getComponent())); + return; + } + + if (part instanceof IconPart) { + node.node("icon").set(Icon.class, ((IconPart) part).getIcon()); + return; + } + + throw new SerializationException("Unknown button content part type: " + part.getClass().getName()); + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java new file mode 100644 index 00000000..be3fada2 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java @@ -0,0 +1,504 @@ +/* + * 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.button; + +import com.google.protobuf.Message; +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.ApolloManager; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.action.RunCommandAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart; +import com.lunarclient.apollo.common.button.content.LiveComponentPart; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.module.ApolloModule; +import com.lunarclient.apollo.network.ButtonNetworkTypes; +import com.lunarclient.apollo.option.SimpleOption; +import com.lunarclient.apollo.player.ApolloPlayer; +import com.lunarclient.apollo.recipients.Recipients; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +/** + * The shared engine behind every button module. + * + * @param the button type + * @param

the protobuf message + * @since 1.2.9 + */ +@RequiredArgsConstructor +public final class ButtonModuleSupport { + + private static final long TICK_MILLIS = 50L; + + private final Set openViewers = ConcurrentHashMap.newKeySet(); + private final Map> liveButtons = new ConcurrentHashMap<>(); + private final AtomicBoolean broadcastStarted = new AtomicBoolean(false); + private final AtomicBoolean ticking = new AtomicBoolean(false); + + private final ApolloModule owner; + private final ButtonSurface surface; + private final SimpleOption broadcastOption; + + /** + * Starts the repeating live-button broadcast task. + * + * @since 1.2.9 + */ + public void startBroadcast() { + if (!this.broadcastStarted.compareAndSet(false, true)) { + return; + } + + try { + Apollo.getPlatform().getScheduler() + .scheduleAsyncRepeating(this::broadcastTick, TICK_MILLIS, TICK_MILLIS, TimeUnit.MILLISECONDS); + } catch (Throwable throwable) { + this.broadcastStarted.set(false); + } + } + + /** + * Validates and displays the buttons, resolving live values per viewer + * and registering live buttons for the periodic broadcast. + * + * @param recipients the recipients that are receiving the packet + * @param buttons the buttons to display + * @since 1.2.9 + */ + public void displayButtons(Recipients recipients, Collection buttons) { + if (buttons.isEmpty()) { + throw new IllegalArgumentException("Button collection must not be empty"); + } + + Set ids = new HashSet<>(buttons.size()); + List live = new ArrayList<>(); + for (B button : buttons) { + this.surface.validate(button); + + if (!ids.add(button.getId())) { + throw new IllegalArgumentException("Duplicate button id '" + button.getId() + "' in display batch"); + } + + if (this.isLiveButton(button)) { + live.add(button); + } + } + + if (live.isEmpty()) { + List

elements = new ArrayList<>(); + for (B button : buttons) { + elements.add(this.surface.toDisplayElement(button, null)); + } + + ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createDisplay(elements)); + this.untrackButtons(recipients, buttons); + return; + } + + recipients.forEach(recipient -> { + ApolloPlayer player = (ApolloPlayer) recipient; + + List

elements = new ArrayList<>(); + for (B button : buttons) { + elements.add(this.surface.toDisplayElement(button, player)); + } + + ApolloManager.getNetworkManager().sendPacket(player, this.surface.createDisplay(elements)); + + long now = System.currentTimeMillis(); + Map tracked = this.liveButtons.computeIfAbsent(player.getUniqueId(), uuid -> new ConcurrentHashMap<>()); + + for (B button : buttons) { + if (this.isLiveButton(button)) { + tracked.put(button.getId(), this.createLiveButton(button.getContent(), button.getTooltip(), now)); + } else { + tracked.remove(button.getId()); + } + } + }); + } + + private void untrackButtons(Recipients recipients, Collection buttons) { + recipients.forEach(recipient -> { + Map tracked = this.liveButtons.get(((ApolloPlayer) recipient).getUniqueId()); + if (tracked == null) { + return; + } + + for (B button : buttons) { + tracked.remove(button.getId()); + } + }); + } + + /** + * Removes a button by id and drops it from live tracking. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the button id + * @since 1.2.9 + */ + public void removeButton(Recipients recipients, String buttonId) { + ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createRemove(buttonId)); + + recipients.forEach(recipient -> { + Map tracked = this.liveButtons.get(((ApolloPlayer) recipient).getUniqueId()); + if (tracked != null) { + tracked.remove(buttonId); + } + }); + } + + /** + * Resets all buttons and drops the recipients live tracking. + * + * @param recipients the recipients that are receiving the packet + * @since 1.2.9 + */ + public void resetButtons(Recipients recipients) { + ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createReset()); + recipients.forEach(recipient -> this.liveButtons.remove(((ApolloPlayer) recipient).getUniqueId())); + } + + /** + * Pushes an update for a displayed button. + * + * @param recipients the recipients that are receiving the packet + * @param buttonId the button id + * @param content the new content, or {@code null} to keep the current one + * @param updateTooltip whether the tooltip should be replaced at all + * @param tooltip the new tooltip, or {@code null} to clear it (when + * {@code updateTooltip} is {@code true}) + * @since 1.2.9 + */ + public void pushUpdate(Recipients recipients, String buttonId, + @Nullable ApolloButtonContent content, boolean updateTooltip, + @Nullable ApolloButtonTooltip tooltip) { + if (buttonId.isEmpty()) { + throw new IllegalArgumentException("ApolloButton#id must not be empty"); + } + + boolean live = (content != null && content.isLive()) || (updateTooltip && tooltip != null && tooltip.isLive()); + + if (!live) { + ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(content, tooltip, updateTooltip, null); + ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createUpdate(buttonId, update)); + } else { + recipients.forEach(recipient -> { + ApolloPlayer player = (ApolloPlayer) recipient; + + ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(content, tooltip, updateTooltip, player); + ApolloManager.getNetworkManager().sendPacket(player, this.surface.createUpdate(buttonId, update)); + }); + } + + recipients.forEach(recipient -> { + Map tracked = this.liveButtons + .computeIfAbsent(((ApolloPlayer) recipient).getUniqueId(), uuid -> new ConcurrentHashMap<>()); + + long now = System.currentTimeMillis(); + tracked.compute(buttonId, (id, previous) -> { + ApolloButtonContent newContent = content != null ? content + : previous != null ? previous.content : null; + ApolloButtonTooltip newTooltip = updateTooltip ? tooltip + : previous != null ? previous.tooltip : null; + + if (newContent == null || (!newContent.isLive() && (newTooltip == null || !newTooltip.isLive()))) { + return null; + } + + return this.createLiveButton(newContent, newTooltip, now); + }); + }); + } + + /** + * Marks a player's surface as open and pushes an immediate live refresh + * for their tracked buttons; called from the surface's open event. + * + * @param player the player whose surface opened + * @since 1.2.9 + */ + public void handleOpen(ApolloPlayer player) { + if (!this.owner.isEnabled()) { + return; + } + + this.openViewers.add(player.getUniqueId()); + + if (!this.owner.getOptions().get(this.broadcastOption)) { + return; + } + + Map tracked = this.liveButtons.get(player.getUniqueId()); + if (tracked != null && !tracked.isEmpty()) { + this.sendLiveUpdates(player, tracked, System.currentTimeMillis(), true); + } + } + + /** + * Marks a player's surface as closed; called from the surface's close + * event. + * + * @param playerIdentifier the player whose surface closed + * @since 1.2.9 + */ + public void handleClose(UUID playerIdentifier) { + this.openViewers.remove(playerIdentifier); + } + + /** + * Drops all state for a disconnecting player. + * + * @param playerIdentifier the unregistering player + * @since 1.2.9 + */ + public void handleUnregister(UUID playerIdentifier) { + this.openViewers.remove(playerIdentifier); + this.liveButtons.remove(playerIdentifier); + } + + private void broadcastTick() { + if (!this.ticking.compareAndSet(false, true)) { + return; + } + + try { + if (!this.owner.isEnabled()) { + this.liveButtons.clear(); + this.openViewers.clear(); + return; + } + + if (!this.owner.getOptions().get(this.broadcastOption) || this.liveButtons.isEmpty()) { + return; + } + + long now = System.currentTimeMillis(); + boolean trackingActive = this.surface.isOpenTrackingActive(); + + Iterator>> iterator = this.liveButtons.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if (entry.getValue().isEmpty()) { + iterator.remove(); + continue; + } + + if (trackingActive && !this.openViewers.contains(entry.getKey())) { + continue; + } + + if (!this.anyUpdateDue(entry.getValue(), now)) { + continue; + } + + ApolloPlayer player = Apollo.getPlayerManager().getPlayer(entry.getKey()).orElse(null); + if (player == null) { + iterator.remove(); + continue; + } + + try { + this.sendLiveUpdates(player, entry.getValue(), now, false); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + } finally { + this.ticking.set(false); + } + } + + private long millisToTicks(Duration interval) { + return Math.max(1L, interval.toMillis() / TICK_MILLIS) * TICK_MILLIS; + } + + private LiveButton createLiveButton(ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip, long now) { + long contentInterval = 0L; + for (ApolloButtonContentPart part : content.getParts()) { + if (!(part instanceof LiveComponentPart)) { + continue; + } + + LiveComponentPart livePart = (LiveComponentPart) part; + long interval = this.millisToTicks(livePart.getUpdateInterval()); + + if (contentInterval == 0L || interval < contentInterval) { + contentInterval = interval; + } + } + + Duration tooltipUpdateInterval = tooltip != null ? tooltip.getUpdateInterval() : null; + long tooltipInterval = tooltipUpdateInterval != null ? this.millisToTicks(tooltipUpdateInterval) : 0L; + + return new LiveButton(content, tooltip, contentInterval, tooltipInterval, now); + } + + private boolean anyUpdateDue(Map buttons, long now) { + for (LiveButton button : buttons.values()) { + if (button.isContentDue(now) || button.isTooltipDue(now)) { + return true; + } + } + + return false; + } + + private void sendLiveUpdates(ApolloPlayer player, Map buttons, long now, boolean force) { + for (Map.Entry entry : buttons.entrySet()) { + LiveButton button = entry.getValue(); + + boolean contentDue = force ? button.contentIntervalMillis > 0L : button.isContentDue(now); + boolean tooltipDue = force ? button.tooltipIntervalMillis > 0L : button.isTooltipDue(now); + if (!contentDue && !tooltipDue) { + continue; + } + + ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(contentDue + ? button.content : null, button.tooltip, tooltipDue, player); + + ApolloManager.getNetworkManager().sendPacket(player, this.surface.createUpdate(entry.getKey(), update)); + + if (contentDue) { + button.nextContentUpdate = now + button.contentIntervalMillis; + } + + if (tooltipDue) { + button.nextTooltipUpdate = now + button.tooltipIntervalMillis; + } + } + } + + private boolean isLiveButton(ApolloButton button) { + ApolloButtonTooltip tooltip = button.getTooltip(); + return button.getContent().isLive() || (tooltip != null && tooltip.isLive()); + } + + private static final class LiveButton { + + private final ApolloButtonContent content; + private final @Nullable ApolloButtonTooltip tooltip; + private final long contentIntervalMillis; + private final long tooltipIntervalMillis; + private volatile long nextContentUpdate; + private volatile long nextTooltipUpdate; + + private LiveButton(ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip, + long contentIntervalMillis, long tooltipIntervalMillis, long now) { + this.content = content; + this.tooltip = tooltip; + this.contentIntervalMillis = contentIntervalMillis; + this.tooltipIntervalMillis = tooltipIntervalMillis; + this.nextContentUpdate = now + contentIntervalMillis; + this.nextTooltipUpdate = now + tooltipIntervalMillis; + } + + private boolean isContentDue(long now) { + return this.contentIntervalMillis > 0L && now >= this.nextContentUpdate; + } + + private boolean isTooltipDue(long now) { + return this.tooltipIntervalMillis > 0L && now >= this.nextTooltipUpdate; + } + } + + /** + * Validates the button properties. + * + * @param button the button to validate + * @param boxWidth the surface container width + * @param boxHeight the surface container height + * @since 1.2.9 + */ + public static void validateCommon(ApolloButton button, float boxWidth, float boxHeight) { + requireSet(button.getId(), "ApolloButton#id"); + requireSet(button.getPosition(), "ApolloButton#position"); + requireSet(button.getSize(), "ApolloButton#size"); + requireSet(button.getShape(), "ApolloButton#shape"); + requireSet(button.getBackgroundColor(), "ApolloButton#backgroundColor"); + requireSet(button.getBorderColor(), "ApolloButton#borderColor"); + requireSet(button.getContent(), "ApolloButton#content"); + + if (button.getId().isEmpty()) { + throw new IllegalArgumentException("ApolloButton#id must not be empty"); + } + + HudPosition position = button.getPosition(); + ApolloButtonSize size = button.getSize(); + + if (!Float.isFinite(position.getX()) || !Float.isFinite(position.getY()) + || !Float.isFinite(size.getWidth()) || !Float.isFinite(size.getHeight())) { + throw new IllegalArgumentException("ApolloButton position and size must be finite"); + } + + if (size.getWidth() <= 0.0F || size.getHeight() <= 0.0F) { + throw new IllegalArgumentException("ApolloButton#size width and height must be greater than 0"); + } + + if (position.getX() < 0.0F || position.getY() < 0.0F + || position.getX() + size.getWidth() > boxWidth + || position.getY() + size.getHeight() > boxHeight) { + throw new IllegalArgumentException("ApolloButton must fit within the " + boxWidth + "x" + boxHeight + " button box"); + } + + ApolloButtonAction onClick = button.getOnClick(); + if (onClick instanceof RunCommandAction && !((RunCommandAction) onClick).getCommand().startsWith("/")) { + throw new IllegalArgumentException("RunCommandAction#command must start with '/'"); + } + } + + /** + * Throws a {@link IllegalArgumentException} when the value is {@code null}. + * + * @param value the value to check + * @param name the field name + * @since 1.2.9 + */ + public static void requireSet(@Nullable Object value, String name) { + if (value == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java new file mode 100644 index 00000000..75fa9701 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java @@ -0,0 +1,105 @@ +/* + * 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.button; + +import com.google.protobuf.Message; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.util.List; +import org.jetbrains.annotations.Nullable; + +/** + * The per-surface strategy behind {@link ButtonModuleSupport}. + * + * @param the button type + * @param

the protobuf message + * @since 1.2.9 + */ +public interface ButtonSurface { + + /** + * Validates a button before it is sent, throwing {@link IllegalArgumentException} on invalid buttons. + * + * @param button the button to validate + * @since 1.2.9 + */ + void validate(B button); + + /** + * Builds the surface wrapper element for a button, embedding the shared + * {@code button.v1.Button} payload alongside the surface placement fields. + * + * @param button the button to serialize + * @param viewer the viewing player live values are resolved for, or {@code null} + * @return the wrapper element + * @since 1.2.9 + */ + P toDisplayElement(B button, @Nullable ApolloPlayer viewer); + + /** + * Builds the surface display message from wrapper elements. + * + * @param elements the wrapper elements + * @return the display message + * @since 1.2.9 + */ + Message createDisplay(List

elements); + + /** + * Builds the surface update message for a button id. + * + * @param buttonId the button id + * @param update the shared update fragment + * @return the update message + * @since 1.2.9 + */ + Message createUpdate(String buttonId, ButtonUpdate update); + + /** + * Builds the surface remove message for a button id. + * + * @param buttonId the button id + * @return the remove message + * @since 1.2.9 + */ + Message createRemove(String buttonId); + + /** + * Builds the surface reset message. + * + * @return the reset message + * @since 1.2.9 + */ + Message createReset(); + + /** + * Returns whether per-player surface open/close tracking is active. + * + * @return whether open tracking is active + * @since 1.2.9 + */ + boolean isOpenTrackingActive(); + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java new file mode 100644 index 00000000..e20999e1 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java @@ -0,0 +1,57 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.chat; + +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.module.button.ApolloButtonSerializer; +import java.awt.Color; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; + +/** + * Reads and writes {@link ChatButton}s from the module configuration; see + * {@link ApolloButtonSerializer} for the shared button fields. + * + * @since 1.2.9 + */ +public final class ChatButtonSerializer extends ApolloButtonSerializer { + + @Override + protected ApolloButton.ApolloButtonBuilder createBuilder(ConfigurationNode node) throws SerializationException { + ChatButton.ChatButtonBuilder builder = ChatButton.builder(); + + Color background = node.node("background-color").get(Color.class); + if (background != null) { + builder.backgroundColor(background); + } + + Color border = node.node("border-color").get(Color.class); + if (border != null) { + builder.borderColor(border); + } + + return builder; + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java index ae4ee548..7b1a7eec 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java @@ -23,20 +23,66 @@ */ package com.lunarclient.apollo.module.chat; +import com.google.protobuf.Message; +import com.lunarclient.apollo.Apollo; import com.lunarclient.apollo.ApolloManager; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.chat.v1.DisplayChatButtonsMessage; import com.lunarclient.apollo.chat.v1.DisplayLiveChatMessageMessage; +import com.lunarclient.apollo.chat.v1.RemoveChatButtonMessage; import com.lunarclient.apollo.chat.v1.RemoveLiveChatMessageMessage; +import com.lunarclient.apollo.chat.v1.ResetChatButtonsMessage; +import com.lunarclient.apollo.chat.v1.UpdateChatButtonMessage; import com.lunarclient.apollo.common.ApolloComponent; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatCloseEvent; +import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatOpenEvent; +import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent; +import com.lunarclient.apollo.event.player.ApolloUnregisterPlayerEvent; +import com.lunarclient.apollo.module.button.ButtonModuleSupport; +import com.lunarclient.apollo.module.button.ButtonSurface; +import com.lunarclient.apollo.module.packetenrichment.PacketEnrichmentModule; +import com.lunarclient.apollo.network.ButtonNetworkTypes; +import com.lunarclient.apollo.option.Options; +import com.lunarclient.apollo.option.config.Serializer; +import com.lunarclient.apollo.player.ApolloPlayer; import com.lunarclient.apollo.recipients.Recipients; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import lombok.NonNull; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; /** * Provides the chat module. * * @since 1.0.2 */ -public final class ChatModuleImpl extends ChatModule { +public final class ChatModuleImpl extends ChatModule implements ButtonSurface, Serializer { + + private final ButtonModuleSupport support = + new ButtonModuleSupport<>(this, this, ChatModule.BROADCAST_LIVE_BUTTONS); + + /** + * Creates a new instance of {@link ChatModuleImpl}. + * + * @since 1.2.9 + */ + public ChatModuleImpl() { + super(); + this.serializer(ChatButton.class, new ChatButtonSerializer()); + this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister); + this.handle(ApolloPlayerChatOpenEvent.class, event -> this.support.handleOpen(event.getPlayer())); + this.handle(ApolloPlayerChatCloseEvent.class, event -> this.support.handleClose(event.getPlayer().getUniqueId())); + this.handle(ApolloUnregisterPlayerEvent.class, event -> this.support.handleUnregister(event.getPlayer().getUniqueId())); + } + + @Override + protected void onEnable() { + this.support.startBroadcast(); + } @Override public void displayLiveChatMessage(@NonNull Recipients recipients, @NonNull Component text, int messageId) { @@ -57,4 +103,126 @@ public void removeLiveChatMessage(@NonNull Recipients recipients, int messageId) ApolloManager.getNetworkManager().sendPacket(recipients, message); } + @Override + public void displayChatButtons(@NonNull Recipients recipients, @NonNull Collection buttons) { + if (buttons.size() > ChatButton.MAX_BUTTONS) { + throw new IllegalArgumentException("ChatButton batches support at most " + ChatButton.MAX_BUTTONS + " buttons"); + } + + this.support.displayButtons(recipients, buttons); + } + + @Override + public void displayChatButton(@NonNull Recipients recipients, @NonNull ChatButton button) { + this.support.displayButtons(recipients, Collections.singleton(button)); + } + + @Override + public void removeChatButton(@NonNull Recipients recipients, @NonNull String buttonId) { + this.support.removeButton(recipients, buttonId); + } + + @Override + public void removeChatButton(@NonNull Recipients recipients, @NonNull ChatButton button) { + this.support.removeButton(recipients, button.getId()); + } + + @Override + public void resetChatButtons(@NonNull Recipients recipients) { + this.support.resetButtons(recipients); + } + + @Override + public void updateChatButton(@NonNull Recipients recipients, @NonNull String buttonId, + @NonNull ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip) { + this.support.pushUpdate(recipients, buttonId, content, true, tooltip); + } + + @Override + public void updateChatButtonContent(@NonNull Recipients recipients, @NonNull String buttonId, @NonNull ApolloButtonContent content) { + this.support.pushUpdate(recipients, buttonId, content, false, null); + } + + @Override + public void updateChatButtonTooltip(@NonNull Recipients recipients, @NonNull String buttonId, @Nullable ApolloButtonTooltip tooltip) { + this.support.pushUpdate(recipients, buttonId, null, true, tooltip); + } + + private void onPlayerRegister(ApolloRegisterPlayerEvent event) { + if (!this.isEnabled() || !this.getOptions().get(ChatModule.SEND_DEFAULT_BUTTONS)) { + return; + } + + ApolloPlayer player = event.getPlayer(); + List buttons = this.getOptions().get(player, ChatModule.DEFAULT_BUTTONS); + if (buttons == null || buttons.isEmpty()) { + return; + } + + try { + this.support.displayButtons(player, buttons); + } catch (IllegalArgumentException exception) { + Apollo.getPlatform().getPlatformLogger() + .warning("Skipping the default chat buttons: " + exception.getMessage()); + } + } + + @Override + public void validate(ChatButton button) { + ButtonModuleSupport.validateCommon(button, ChatButton.BOX_WIDTH, ChatButton.BOX_HEIGHT); + } + + @Override + public com.lunarclient.apollo.chat.v1.ChatButton toDisplayElement(ChatButton button, @Nullable ApolloPlayer viewer) { + return com.lunarclient.apollo.chat.v1.ChatButton.newBuilder() + .setButton(ButtonNetworkTypes.toProtobuf(button, viewer)) + .build(); + } + + @Override + public Message createDisplay(List elements) { + return DisplayChatButtonsMessage.newBuilder() + .addAllChatButtons(elements) + .build(); + } + + @Override + public Message createUpdate(String buttonId, ButtonUpdate update) { + return UpdateChatButtonMessage.newBuilder() + .setId(buttonId) + .setUpdate(update) + .build(); + } + + @Override + public Message createRemove(String buttonId) { + return RemoveChatButtonMessage.newBuilder() + .setId(buttonId) + .build(); + } + + @Override + public Message createReset() { + return ResetChatButtonsMessage.getDefaultInstance(); + } + + /** + * Returns whether open-chat tracking is available. + * + * @return whether open-chat tracking is active + */ + @Override + public boolean isOpenTrackingActive() { + PacketEnrichmentModule packetEnrichment = Apollo.getModuleManager().getModule(PacketEnrichmentModule.class); + if (packetEnrichment == null || !packetEnrichment.isEnabled()) { + return false; + } + + Options options = packetEnrichment.getOptions(); + return options.get(PacketEnrichmentModule.PLAYER_CHAT_OPEN_PACKET) + && options.get(PacketEnrichmentModule.PLAYER_CHAT_CLOSE_PACKET) + && options.get(PacketEnrichmentModule.PLAYER_CHAT_OPEN_EVENT) + && options.get(PacketEnrichmentModule.PLAYER_CHAT_CLOSE_EVENT); + } + } diff --git a/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java new file mode 100644 index 00000000..505c00b6 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java @@ -0,0 +1,66 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.module.inventory; + +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.module.button.ApolloButtonSerializer; +import java.awt.Color; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; + +/** + * Reads and writes {@link InventoryButton}s from the module configuration; + * see {@link ApolloButtonSerializer} for the shared button fields. + * + * @since 1.2.9 + */ +public final class InventoryButtonSerializer extends ApolloButtonSerializer { + + @Override + protected ApolloButton.ApolloButtonBuilder createBuilder(ConfigurationNode node) throws SerializationException { + InventoryButton.InventoryButtonBuilder builder = InventoryButton.builder() + .inventoryType(this.parseEnum(InventoryType.class, + this.virtualNode(node, "inventory-type").getString(), "inventory-type")) + .box(this.parseEnum(InventoryButtonBox.class, this.virtualNode(node, "box").getString(), "box")); + + Color background = node.node("background-color").get(Color.class); + if (background != null) { + builder.backgroundColor(background); + } + + Color border = node.node("border-color").get(Color.class); + if (border != null) { + builder.borderColor(border); + } + + return builder; + } + + @Override + protected void serializeSurface(InventoryButton button, ConfigurationNode node) throws SerializationException { + node.node("inventory-type").set(button.getInventoryType().name()); + node.node("box").set(button.getBox().name()); + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java new file mode 100644 index 00000000..87590b87 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java @@ -0,0 +1,221 @@ +/* + * 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.inventory; + +import com.google.protobuf.Message; +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryCloseEvent; +import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryOpenEvent; +import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent; +import com.lunarclient.apollo.event.player.ApolloUnregisterPlayerEvent; +import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage; +import com.lunarclient.apollo.inventory.v1.ResetInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage; +import com.lunarclient.apollo.module.button.ButtonModuleSupport; +import com.lunarclient.apollo.module.button.ButtonSurface; +import com.lunarclient.apollo.module.packetenrichment.PacketEnrichmentModule; +import com.lunarclient.apollo.network.ButtonNetworkTypes; +import com.lunarclient.apollo.option.Options; +import com.lunarclient.apollo.option.config.Serializer; +import com.lunarclient.apollo.player.ApolloPlayer; +import com.lunarclient.apollo.recipients.Recipients; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +/** + * Provides the inventory module. + * + * @since 1.2.9 + */ +public final class InventoryModuleImpl extends InventoryModule implements ButtonSurface, Serializer { + + private final ButtonModuleSupport support = + new ButtonModuleSupport<>(this, this, InventoryModule.BROADCAST_LIVE_BUTTONS); + + /** + * Creates a new instance of {@link InventoryModuleImpl}. + * + * @since 1.2.9 + */ + public InventoryModuleImpl() { + super(); + this.serializer(InventoryButton.class, new InventoryButtonSerializer()); + this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister); + this.handle(ApolloPlayerInventoryOpenEvent.class, event -> this.support.handleOpen(event.getPlayer())); + this.handle(ApolloPlayerInventoryCloseEvent.class, event -> this.support.handleClose(event.getPlayer().getUniqueId())); + this.handle(ApolloUnregisterPlayerEvent.class, event -> this.support.handleUnregister(event.getPlayer().getUniqueId())); + } + + @Override + protected void onEnable() { + this.support.startBroadcast(); + } + + @Override + public void displayInventoryButtons(@NonNull Recipients recipients, @NonNull Collection buttons) { + int leftButtons = 0; + int rightButtons = 0; + for (InventoryButton button : buttons) { + if (button.getBox() == InventoryButtonBox.LEFT) { + leftButtons++; + } else if (button.getBox() == InventoryButtonBox.RIGHT) { + rightButtons++; + } + } + + if (leftButtons > InventoryButton.MAX_BUTTONS_PER_BOX || rightButtons > InventoryButton.MAX_BUTTONS_PER_BOX) { + throw new IllegalArgumentException("InventoryButton batches support at most " + InventoryButton.MAX_BUTTONS_PER_BOX + " buttons per box"); + } + + this.support.displayButtons(recipients, buttons); + } + + @Override + public void displayInventoryButton(@NonNull Recipients recipients, @NonNull InventoryButton button) { + this.support.displayButtons(recipients, Collections.singleton(button)); + } + + @Override + public void removeInventoryButton(@NonNull Recipients recipients, @NonNull String buttonId) { + this.support.removeButton(recipients, buttonId); + } + + @Override + public void removeInventoryButton(@NonNull Recipients recipients, @NonNull InventoryButton button) { + this.support.removeButton(recipients, button.getId()); + } + + @Override + public void resetInventoryButtons(@NonNull Recipients recipients) { + this.support.resetButtons(recipients); + } + + @Override + public void updateInventoryButton(@NonNull Recipients recipients, @NonNull String buttonId, + @NonNull ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip) { + this.support.pushUpdate(recipients, buttonId, content, true, tooltip); + } + + @Override + public void updateInventoryButtonContent(@NonNull Recipients recipients, @NonNull String buttonId, @NonNull ApolloButtonContent content) { + this.support.pushUpdate(recipients, buttonId, content, false, null); + } + + @Override + public void updateInventoryButtonTooltip(@NonNull Recipients recipients, @NonNull String buttonId, @Nullable ApolloButtonTooltip tooltip) { + this.support.pushUpdate(recipients, buttonId, null, true, tooltip); + } + + private void onPlayerRegister(ApolloRegisterPlayerEvent event) { + if (!this.isEnabled() || !this.getOptions().get(InventoryModule.SEND_DEFAULT_BUTTONS)) { + return; + } + + ApolloPlayer player = event.getPlayer(); + List buttons = this.getOptions().get(player, InventoryModule.DEFAULT_BUTTONS); + if (buttons == null || buttons.isEmpty()) { + return; + } + + try { + this.support.displayButtons(player, buttons); + } catch (IllegalArgumentException exception) { + Apollo.getPlatform().getPlatformLogger() + .warning("Skipping the default inventory buttons: " + exception.getMessage()); + } + } + + @Override + public void validate(InventoryButton button) { + ButtonModuleSupport.validateCommon(button, InventoryButton.BOX_WIDTH, InventoryButton.BOX_HEIGHT); + + ButtonModuleSupport.requireSet(button.getInventoryType(), "InventoryButton#inventoryType"); + ButtonModuleSupport.requireSet(button.getBox(), "InventoryButton#box"); + } + + @Override + public com.lunarclient.apollo.inventory.v1.InventoryButton toDisplayElement(InventoryButton button, @Nullable ApolloPlayer viewer) { + return com.lunarclient.apollo.inventory.v1.InventoryButton.newBuilder() + .setButton(ButtonNetworkTypes.toProtobuf(button, viewer)) + .setInventoryType(com.lunarclient.apollo.inventory.v1.InventoryType + .forNumber(button.getInventoryType().ordinal() + 1)) + .setBox(com.lunarclient.apollo.inventory.v1.InventoryButtonBox + .forNumber(button.getBox().ordinal() + 1)) + .build(); + } + + @Override + public Message createDisplay(List elements) { + return DisplayInventoryButtonsMessage.newBuilder() + .addAllInventoryButtons(elements) + .build(); + } + + @Override + public Message createUpdate(String buttonId, ButtonUpdate update) { + return UpdateInventoryButtonMessage.newBuilder() + .setId(buttonId) + .setUpdate(update) + .build(); + } + + @Override + public Message createRemove(String buttonId) { + return RemoveInventoryButtonMessage.newBuilder() + .setId(buttonId) + .build(); + } + + @Override + public Message createReset() { + return ResetInventoryButtonsMessage.getDefaultInstance(); + } + + /** + * Returns whether open-inventory tracking is available. + * + * @return whether open-inventory tracking is active + */ + @Override + public boolean isOpenTrackingActive() { + PacketEnrichmentModule packetEnrichment = Apollo.getModuleManager().getModule(PacketEnrichmentModule.class); + if (packetEnrichment == null || !packetEnrichment.isEnabled()) { + return false; + } + + Options options = packetEnrichment.getOptions(); + return options.get(PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_PACKET) + && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_PACKET) + && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_EVENT) + && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_EVENT); + } + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java index cbc97f0d..8bbe8004 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java @@ -27,6 +27,8 @@ import com.lunarclient.apollo.event.EventBus; import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatCloseEvent; import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatOpenEvent; +import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryCloseEvent; +import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryOpenEvent; import com.lunarclient.apollo.event.packetenrichment.melee.ApolloPlayerAttackEvent; import com.lunarclient.apollo.event.packetenrichment.world.ApolloPlayerUseItemBucketEvent; import com.lunarclient.apollo.event.packetenrichment.world.ApolloPlayerUseItemEvent; @@ -35,6 +37,8 @@ import com.lunarclient.apollo.packetenrichment.v1.PlayerAttackMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerChatCloseMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerChatOpenMessage; +import com.lunarclient.apollo.packetenrichment.v1.PlayerInventoryCloseMessage; +import com.lunarclient.apollo.packetenrichment.v1.PlayerInventoryOpenMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerUseItemBucketMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerUseItemMessage; @@ -110,6 +114,36 @@ private void onReceivePacket(ApolloReceivePacketEvent event) { }); } + if (options.get(PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_EVENT)) { + event.unpack(PlayerInventoryOpenMessage.class).ifPresent(packet -> { + ApolloPlayerInventoryOpenEvent playerInventoryOpenEvent = new ApolloPlayerInventoryOpenEvent( + event.getPlayer(), + NetworkTypes.fromProtobuf(packet.getPacketInfo().getInstantiationTime()), + NetworkTypes.fromProtobuf(packet.getPlayerInfo())); + + EventBus.EventResult result = EventBus.getBus().post(playerInventoryOpenEvent); + + for (Throwable throwable : result.getThrowing()) { + throwable.printStackTrace(); + } + }); + } + + if (options.get(PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_EVENT)) { + event.unpack(PlayerInventoryCloseMessage.class).ifPresent(packet -> { + ApolloPlayerInventoryCloseEvent playerInventoryCloseEvent = new ApolloPlayerInventoryCloseEvent( + event.getPlayer(), + NetworkTypes.fromProtobuf(packet.getPacketInfo().getInstantiationTime()), + NetworkTypes.fromProtobuf(packet.getPlayerInfo())); + + EventBus.EventResult result = EventBus.getBus().post(playerInventoryCloseEvent); + + for (Throwable throwable : result.getThrowing()) { + throwable.printStackTrace(); + } + }); + } + if (options.get(PacketEnrichmentModule.PLAYER_USE_ITEM_EVENT)) { event.unpack(PlayerUseItemMessage.class).ifPresent(packet -> { ApolloPlayerUseItemEvent playerUseItemEvent = new ApolloPlayerUseItemEvent( diff --git a/common/src/main/java/com/lunarclient/apollo/network/ButtonNetworkTypes.java b/common/src/main/java/com/lunarclient/apollo/network/ButtonNetworkTypes.java new file mode 100644 index 00000000..3baad24a --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/network/ButtonNetworkTypes.java @@ -0,0 +1,262 @@ +/* + * 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.network; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonClientAction; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonContentPart; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.ApolloComponent; +import com.lunarclient.apollo.common.button.ApolloButton; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.action.ClientAction; +import com.lunarclient.apollo.common.button.action.OpenUrlAction; +import com.lunarclient.apollo.common.button.action.RunCommandAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart; +import com.lunarclient.apollo.common.button.content.ComponentPart; +import com.lunarclient.apollo.common.button.content.IconPart; +import com.lunarclient.apollo.common.button.content.LiveComponentPart; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Serializes the shared {@code common.button} API types into their + * {@code button.v1} protobuf counterparts, resolving live content and + * tooltips for a viewing player. + * + * @since 1.2.9 + */ +public final class ButtonNetworkTypes { + + /** + * Serializes the fields of the given button. + * + * @param button the button to serialize + * @param viewer the viewing player live parts are resolved for, or + * {@code null} when the button has no live parts + * @return the protobuf button + * @since 1.2.9 + */ + public static Button toProtobuf(ApolloButton button, @Nullable ApolloPlayer viewer) { + Button.Builder builder = Button.newBuilder() + .setId(button.getId()) + .setPosition(NetworkTypes.toProtobuf(button.getPosition())) + .setSize(ButtonSize.newBuilder() + .setWidth(button.getSize().getWidth()) + .setHeight(button.getSize().getHeight()) + .build()) + .setShape(ButtonShape.forNumber(button.getShape().ordinal() + 1)) + .setBackgroundColor(NetworkTypes.toProtobuf(button.getBackgroundColor())) + .setBorderColor(NetworkTypes.toProtobuf(button.getBorderColor())) + .setContent(toContentProtobuf(button.getContent(), viewer)); + + ButtonTooltip tooltip = toTooltipProtobuf(button.getTooltip(), viewer); + if (tooltip != null) { + builder.setTooltip(tooltip); + } + + ApolloButtonAction onClick = button.getOnClick(); + if (onClick instanceof RunCommandAction) { + builder.setRunCommand(((RunCommandAction) onClick).getCommand()); + } else if (onClick instanceof OpenUrlAction) { + builder.setOpenUrl(((OpenUrlAction) onClick).getUrl()); + } else if (onClick instanceof ClientAction) { + ClientAction clientAction = (ClientAction) onClick; + builder.setClientAction(ButtonClientAction.forNumber(clientAction.getAction().ordinal() + 1)); + } else if (onClick != null) { + throw new IllegalArgumentException("Unknown button action type: " + onClick.getClass().getName()); + } + + Color hoveredBackgroundColor = button.getHoveredBackgroundColor(); + if (hoveredBackgroundColor != null) { + builder.setHoveredBackgroundColor(NetworkTypes.toProtobuf(hoveredBackgroundColor)); + } + + Color hoveredBorderColor = button.getHoveredBorderColor(); + if (hoveredBorderColor != null) { + builder.setHoveredBorderColor(NetworkTypes.toProtobuf(hoveredBorderColor)); + } + + return builder.build(); + } + + /** + * Serializes button content, resolving live parts for the given viewer. + * + * @param content the content to serialize + * @param viewer the viewing player live parts are resolved for, or + * {@code null} when the content has no live parts + * @return the protobuf content + * @since 1.2.9 + */ + public static ButtonContent toContentProtobuf(ApolloButtonContent content, @Nullable ApolloPlayer viewer) { + return ButtonContent.newBuilder() + .addAllParts(resolveContentParts(content, viewer)) + .setScale(content.getScale()) + .build(); + } + + private static List resolveContentParts(ApolloButtonContent content, + @Nullable ApolloPlayer viewer) { + List parts = new ArrayList<>(); + + for (ApolloButtonContentPart part : content.getParts()) { + ButtonContentPart.Builder partBuilder = ButtonContentPart.newBuilder(); + + if (part instanceof LiveComponentPart) { + Function resolver = ((LiveComponentPart) part).getResolver(); + partBuilder.setAdventureJsonText(ApolloComponent.toJson(resolveLive(resolver, viewer))); + } else if (part instanceof ComponentPart) { + partBuilder.setAdventureJsonText(ApolloComponent.toJson(((ComponentPart) part).getComponent())); + } else if (part instanceof IconPart) { + partBuilder.setIcon(NetworkTypes.toProtobuf(((IconPart) part).getIcon())); + } else { + throw new IllegalArgumentException("Unknown button content part type: " + part.getClass().getName()); + } + + parts.add(partBuilder.build()); + } + + return parts; + } + + /** + * Serializes a button tooltip, resolving live tooltips for the given + * viewer. + * + * @param tooltip the tooltip to serialize, or {@code null} + * @param viewer the viewing player a live tooltip is resolved for, or + * {@code null} when the tooltip is static + * @return the protobuf tooltip + * @since 1.2.9 + */ + public static @Nullable ButtonTooltip toTooltipProtobuf(@Nullable ApolloButtonTooltip tooltip, + @Nullable ApolloPlayer viewer) { + List lines = resolveTooltipLines(tooltip, viewer); + if (lines == null) { + return null; + } + + return ButtonTooltip.newBuilder() + .addAllAdventureJsonLines(lines) + .build(); + } + + /** + * Builds a partial-update fragment. + * + * @param content the new content, or {@code null} to keep the current one + * @param tooltip the new tooltip, or {@code null} to clear it + * @param tooltipPresent whether the tooltip field should be set at all + * @param viewer the viewing player live values are resolved for, or {@code null} + * @return the protobuf update + * @since 1.2.9 + */ + public static ButtonUpdate toUpdateProtobuf(@Nullable ApolloButtonContent content, + @Nullable ApolloButtonTooltip tooltip, + boolean tooltipPresent, + @Nullable ApolloPlayer viewer) { + ButtonUpdate.Builder builder = ButtonUpdate.newBuilder(); + + if (content != null) { + builder.setContent(toContentProtobuf(content, viewer)); + } + + if (tooltipPresent) { + ButtonTooltip tooltipProto = toTooltipProtobuf(tooltip, viewer); + if (tooltipProto != null) { + builder.setTooltip(tooltipProto); + } + } + + return builder.build(); + } + + private static Component resolveLive(Function resolver, @Nullable ApolloPlayer viewer) { + if (viewer == null) { + return Component.empty(); + } + + try { + Component component = resolver.apply(viewer); + return component != null ? component : Component.empty(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return Component.empty(); + } + } + + private static @Nullable List resolveTooltipLines(@Nullable ApolloButtonTooltip tooltip, + @Nullable ApolloPlayer viewer) { + if (tooltip == null) { + return Collections.emptyList(); + } + + List lines = tooltip.getLines(); + + Function> resolver = tooltip.getResolver(); + if (resolver != null && viewer != null) { + lines = null; + try { + lines = resolver.apply(viewer); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + if (lines == null) { + return null; + } + } + + if (lines == null) { + return Collections.emptyList(); + } + + try { + return lines.stream() + .map(ApolloComponent::toJson) + .collect(Collectors.toList()); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return null; + } + } + + private ButtonNetworkTypes() { + } + +} 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 00cb3c65..276175bf 100644 --- a/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java +++ b/common/src/main/java/com/lunarclient/apollo/network/NetworkTypes.java @@ -591,6 +591,10 @@ public static com.lunarclient.apollo.common.v1.ItemStackIcon toProtobuf(ItemStac builder.setProfile(NetworkTypes.toProtobuf(icon.getProfile())); } + if (icon.getPotion() != null) { + builder.setPotion(icon.getPotion()); + } + return builder.build(); } @@ -616,6 +620,10 @@ public static ItemStackIcon fromProtobuf(com.lunarclient.apollo.common.v1.ItemSt builder.profile(NetworkTypes.fromProtobuf(icon.getProfile())); } + if (!icon.getPotion().isEmpty()) { + builder.potion(icon.getPotion()); + } + return builder.build(); } diff --git a/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java b/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java index 4462a872..fbd7c9ca 100644 --- a/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java +++ b/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java @@ -23,8 +23,19 @@ */ package com.lunarclient.apollo.option.config; +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; +import com.lunarclient.apollo.common.icon.SimpleResourceLocationIcon; +import com.lunarclient.apollo.common.profile.Profile; import java.awt.Color; import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.serialize.SerializationException; @@ -43,7 +54,11 @@ public final class CommonSerializers implements Serializer { * @since 1.0.0 */ public CommonSerializers() { + ItemStackIconSerializer itemStackIconSerializer = new ItemStackIconSerializer(); + this.serializer(Color.class, new ColorSerializer()); + this.serializer(Icon.class, new IconSerializer(itemStackIconSerializer)); + this.serializer(ItemStackIcon.class, itemStackIconSerializer); } private static final class ColorSerializer implements TypeSerializer { @@ -87,4 +102,197 @@ public void serialize(Type type, @Nullable Color color, ConfigurationNode node) } } + @RequiredArgsConstructor + private static final class IconSerializer implements TypeSerializer { + + private final ItemStackIconSerializer itemStackIconSerializer; + + @Override + public Icon deserialize(Type type, ConfigurationNode node) throws SerializationException { + if (node.hasChild("name") || node.hasChild("id")) { + return this.itemStackIconSerializer.deserialize(type, node); + } + + if (!node.hasChild("resource-location")) { + throw new SerializationException("Icons require a 'name', 'id' or 'resource-location' field!"); + } + + String resourceLocation = node.node("resource-location").getString(); + if (resourceLocation == null || resourceLocation.isEmpty()) { + throw new SerializationException("Icon 'resource-location' must not be empty!"); + } + + if (node.hasChild("width") || node.hasChild("height") || node.hasChild("min-u") + || node.hasChild("max-u") || node.hasChild("min-v") || node.hasChild("max-v")) { + return AdvancedResourceLocationIcon.builder() + .resourceLocation(resourceLocation) + .width((float) node.node("width").getDouble()) + .height((float) node.node("height").getDouble()) + .minU((float) node.node("min-u").getDouble()) + .maxU((float) node.node("max-u").getDouble(1.0D)) + .minV((float) node.node("min-v").getDouble()) + .maxV((float) node.node("max-v").getDouble(1.0D)) + .build(); + } + + if (node.hasChild("size")) { + return SimpleResourceLocationIcon.builder() + .resourceLocation(resourceLocation) + .size(node.node("size").getInt()) + .build(); + } + + return ResourceLocationIcon.builder() + .resourceLocation(resourceLocation) + .build(); + } + + @Override + public void serialize(Type type, @Nullable Icon icon, ConfigurationNode node) throws SerializationException { + if (icon == null) { + node.raw(null); + return; + } + + if (icon instanceof ItemStackIcon) { + this.itemStackIconSerializer.serialize(type, (ItemStackIcon) icon, node); + return; + } + + if (icon instanceof SimpleResourceLocationIcon) { + SimpleResourceLocationIcon simple = (SimpleResourceLocationIcon) icon; + node.node("resource-location").set(simple.getResourceLocation()); + node.node("size").set(simple.getSize()); + return; + } + + if (icon instanceof AdvancedResourceLocationIcon) { + AdvancedResourceLocationIcon advanced = (AdvancedResourceLocationIcon) icon; + node.node("resource-location").set(advanced.getResourceLocation()); + node.node("width").set((double) advanced.getWidth()); + node.node("height").set((double) advanced.getHeight()); + node.node("min-u").set((double) advanced.getMinU()); + node.node("max-u").set((double) advanced.getMaxU()); + node.node("min-v").set((double) advanced.getMinV()); + node.node("max-v").set((double) advanced.getMaxV()); + return; + } + + if (icon instanceof ResourceLocationIcon) { + node.node("resource-location").set(((ResourceLocationIcon) icon).getResourceLocation()); + return; + } + + throw new SerializationException("Unknown icon type: " + icon.getClass().getName()); + } + } + + private static final class ItemStackIconSerializer implements TypeSerializer { + @Override + public ItemStackIcon deserialize(Type type, ConfigurationNode node) throws SerializationException { + if (!node.hasChild("name") && !node.hasChild("id")) { + throw new SerializationException("Item icons require a 'name' or 'id' field!"); + } + + ItemStackIcon.ItemStackIconBuilder builder = ItemStackIcon.builder(); + if (node.hasChild("name")) { + String name = node.node("name").getString(); + if (name == null || name.isEmpty()) { + throw new SerializationException("Item icon 'name' must not be empty!"); + } + + builder.itemName(name); + } else { + builder.itemId(node.node("id").getInt()); + } + + if (node.hasChild("custom-model-data")) { + builder.customModelData(node.node("custom-model-data").getInt()); + } + + if (node.hasChild("model-data")) { + ConfigurationNode data = node.node("model-data"); + builder.customModelDataObject(CustomModelData.builder() + .floats(data.node("floats").getList(Float.class, Collections.emptyList())) + .flags(data.node("flags").getList(Boolean.class, Collections.emptyList())) + .strings(data.node("strings").getList(String.class, Collections.emptyList())) + .colors(data.node("colors").getList(Integer.class, Collections.emptyList())) + .build()); + } + + if (node.hasChild("potion")) { + builder.potion(node.node("potion").getString()); + } + + if (node.hasChild("profile")) { + builder.profile(this.readProfile(node.node("profile"))); + } + + return builder.build(); + } + + @Override + public void serialize(Type type, @Nullable ItemStackIcon icon, ConfigurationNode node) throws SerializationException { + if (icon == null) { + node.raw(null); + return; + } + + if (icon.getItemName() != null) { + node.node("name").set(icon.getItemName()); + } else { + node.node("id").set(icon.getItemId()); + } + + if (icon.getCustomModelData() != 0) { + node.node("custom-model-data").set(icon.getCustomModelData()); + } + + CustomModelData modelData = icon.getCustomModelDataObject(); + if (modelData != null) { + this.writeList(node.node("model-data", "floats"), Float.class, modelData.getFloats()); + this.writeList(node.node("model-data", "flags"), Boolean.class, modelData.getFlags()); + this.writeList(node.node("model-data", "strings"), String.class, modelData.getStrings()); + this.writeList(node.node("model-data", "colors"), Integer.class, modelData.getColors()); + } + + if (icon.getPotion() != null) { + node.node("potion").set(icon.getPotion()); + } + + Profile profile = icon.getProfile(); + if (profile != null) { + if (profile.getId() != null) { + node.node("profile", "id").set(profile.getId().toString()); + } + + node.node("profile", "texture").set(profile.getTexture()); + node.node("profile", "signature").set(profile.getSignature()); + } + } + + private Profile readProfile(ConfigurationNode node) throws SerializationException { + Profile.ProfileBuilder builder = Profile.builder() + .texture(node.node("texture").getString("")) + .signature(node.node("signature").getString("")); + + String id = node.node("id").getString(); + if (id != null) { + try { + builder.id(UUID.fromString(id)); + } catch (IllegalArgumentException exception) { + throw new SerializationException("Invalid profile id '" + id + "'!"); + } + } + + return builder.build(); + } + + private void writeList(ConfigurationNode node, Class type, List values) throws SerializationException { + if (!values.isEmpty()) { + node.setList(type, values); + } + } + } + } diff --git a/docs/developers/events.mdx b/docs/developers/events.mdx index a8899eee..358b356a 100644 --- a/docs/developers/events.mdx +++ b/docs/developers/events.mdx @@ -218,6 +218,36 @@ _Called when the player opens their chat._ +

+ApolloPlayerInventoryCloseEvent + +### ApolloPlayerInventoryCloseEvent + +_Called when the player closes their inventory._ + +| Field | Description | +| -------------------------- | -------------------------------------------------- | +| `ApolloPlayer player` | The Apollo player that sent the packet. | +| `long instantiationTimeMs` | The unix timestamp when the packet was created. | +| `PlayerInfo playerInfo` | The player's general information. | + +
+ +
+ApolloPlayerInventoryOpenEvent + +### ApolloPlayerInventoryOpenEvent + +_Called when the player opens their inventory._ + +| Field | Description | +| -------------------------- | -------------------------------------------------- | +| `ApolloPlayer player` | The Apollo player that sent the packet. | +| `long instantiationTimeMs` | The unix timestamp when the packet was created. | +| `PlayerInfo playerInfo` | The player's general information. | + +
+
ApolloPlayerAttackEvent diff --git a/docs/developers/lightweight/json/serverbound-packets.mdx b/docs/developers/lightweight/json/serverbound-packets.mdx index fc86cf12..0cc4ad8a 100644 --- a/docs/developers/lightweight/json/serverbound-packets.mdx +++ b/docs/developers/lightweight/json/serverbound-packets.mdx @@ -54,6 +54,10 @@ public class ApolloPacketReceiveJsonListener implements PluginMessageListener { this.onPlayerChatOpen(payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerChatCloseMessage".equals(type)) { this.onPlayerChatClose(payload); + } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryOpenMessage".equals(type)) { + this.onPlayerInventoryOpen(player, payload); + } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryCloseMessage".equals(type)) { + this.onPlayerInventoryClose(player, payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemMessage".equals(type)) { this.onPlayerUseItem(payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemBucketMessage".equals(type)) { @@ -102,6 +106,26 @@ public class ApolloPacketReceiveJsonListener implements PluginMessageListener { this.onPlayerInfo(message.getAsJsonObject("player_info")); } + private void onPlayerInventoryOpen(Player player, JsonObject message) { + long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); + this.onPlayerInfo(message.getAsJsonObject("player_info")); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryJsonExample) { + ((InventoryJsonExample) example).handleInventoryOpen(player); + } + } + + private void onPlayerInventoryClose(Player player, JsonObject message) { + long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); + this.onPlayerInfo(message.getAsJsonObject("player_info")); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryJsonExample) { + ((InventoryJsonExample) example).handleInventoryClose(player); + } + } + private void onPlayerUseItem(JsonObject message) { long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); this.onPlayerInfo(message.getAsJsonObject("player_info")); diff --git a/docs/developers/lightweight/protobuf/serverbound-packets.mdx b/docs/developers/lightweight/protobuf/serverbound-packets.mdx index 6690301b..8a75188a 100644 --- a/docs/developers/lightweight/protobuf/serverbound-packets.mdx +++ b/docs/developers/lightweight/protobuf/serverbound-packets.mdx @@ -31,6 +31,10 @@ public class ApolloPacketReceiveProtoListener implements PluginMessageListener { this.onPlayerChatOpen(any.unpack(PlayerChatOpenMessage.class)); } else if (any.is(PlayerChatCloseMessage.class)) { this.onPlayerChatClose(any.unpack(PlayerChatCloseMessage.class)); + } else if (any.is(PlayerInventoryOpenMessage.class)) { + this.onPlayerInventoryOpen(player, any.unpack(PlayerInventoryOpenMessage.class)); + } else if (any.is(PlayerInventoryCloseMessage.class)) { + this.onPlayerInventoryClose(player, any.unpack(PlayerInventoryCloseMessage.class)); } else if (any.is(PlayerUseItemMessage.class)) { this.onPlayerUseItem(any.unpack(PlayerUseItemMessage.class)); } else if (any.is(PlayerUseItemBucketMessage.class)) { @@ -83,6 +87,30 @@ public class ApolloPacketReceiveProtoListener implements PluginMessageListener { this.onPlayerInfo(playerInfo); } + private void onPlayerInventoryOpen(Player player, PlayerInventoryOpenMessage message) { + long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); + + PlayerInfo playerInfo = message.getPlayerInfo(); + this.onPlayerInfo(playerInfo); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryProtoExample) { + ((InventoryProtoExample) example).handleInventoryOpen(player); + } + } + + private void onPlayerInventoryClose(Player player, PlayerInventoryCloseMessage message) { + long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); + + PlayerInfo playerInfo = message.getPlayerInfo(); + this.onPlayerInfo(playerInfo); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryProtoExample) { + ((InventoryProtoExample) example).handleInventoryClose(player); + } + } + private void onPlayerUseItem(PlayerUseItemMessage message) { long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); diff --git a/docs/developers/modules/chat.mdx b/docs/developers/modules/chat.mdx index 6065ed97..4dd6fdbf 100644 --- a/docs/developers/modules/chat.mdx +++ b/docs/developers/modules/chat.mdx @@ -8,6 +8,10 @@ The chat module allows you to interact with and modify users chat feeds. - Adds the ability to simulate live updating messages - Grants the ability to remove specific messages for a player +- Display clickable chat buttons in a fixed-size box between the chat input field and the chat log. + - Ability to mix adventure components and icons inside a single button. + - Ability to customize the button shape, size, position, colors and hover colors. + - Ability to run commands or open URLs on click. ![Chat Module Example](/modules/chat/overview.gif#center) @@ -285,3 +289,664 @@ public void removeLiveChatMessageExample() { +## Chat Buttons + +Display server-driven clickable buttons on the chat screen. Buttons live in a single invisible fixed-size box +(`320x20` GUI-scaled pixels) anchored in the strip between the chat input field and the chat log, visible while the chat screen is open. + +Chat buttons are built on Apollo's shared button system: `ChatButton` extends `ApolloButton`. +Read the [buttons utilities page](/apollo/developers/utilities/buttons) for the shared +concepts: content, tooltips, click actions, shapes, sizes, colors and the update semantics. + + + Displaying buttons with the same id replaces the previous button. + + +### Sample Code +Explore each integration by cycling through each tab, to find the best fit for your requirements and needs. + + + + + + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + +### Displaying Chat Buttons + +```java +public void displayChatButtonsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + ChatButton teamChat = ChatButton.builder() + .id("team-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SHIELD") + .build()) + .append(Component.text("Team Chat", NamedTextColor.GREEN)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel team")) + .build(); + + this.chatModule.displayChatButtons(apolloPlayer, Arrays.asList(teamChat)); + }); +} +``` + +### Removing a Chat Button + +```java +public void removeChatButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.chatModule.removeChatButton(apolloPlayer, "team-chat")); +} +``` + +### Resetting all Chat Buttons + +```java +public void resetChatButtonsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.chatModule::resetChatButtons); +} +``` + +### Updating a Chat Button + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateChatButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> this.chatModule.updateChatButtonContent(apolloPlayer, "public-chat", + ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PAPER") + .build()) + .append(Component.text("Public Chat", NamedTextColor.AQUA)) + .scale(1.0F) + .build())); +} +``` + +### Live Chat Buttons + +Content parts and tooltips can be **live**: instead of a static value, you provide a resolver that is called +for each viewing player and re-broadcast automatically by the module (see [Live Button Broadcast](#live-button-broadcast) below). + +```java +public void displayLiveChatButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + ChatButton unread = ChatButton.builder() + .id("unread") + .position(HudPosition.of(0, 2)) + .size(ChatButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(Component.text("Unread: ", NamedTextColor.GRAY)) + .append(apolloViewer -> Component.text(getUnreadCount(apolloViewer.getUniqueId()), NamedTextColor.RED), Duration.ofSeconds(1)) + .scale(0.85F) + .build()) + .build(); + + this.chatModule.displayChatButton(apolloPlayer, unread); + }); +} +``` + +### `ChatButton` Options + +`ChatButton` extends `ApolloButton`. All shared options: `.id(...)`, `.shape(...)`, `.backgroundColor(...)` & `.borderColor(...)`, +`.content(...)`, `.tooltip(...)`, `.onClick(...)` and the hovered color overrides are documented on the [buttons utilities page](/apollo/developers/utilities/buttons), +along with the `ApolloButtonContent`, `ApolloButtonTooltip` and `ApolloButtonAction` builders used above. + +`.position(HudPosition)` the button position, in GUI-scaled pixels. The button must fit inside the `320x20` box (`ChatButton.BOX_WIDTH` x `ChatButton.BOX_HEIGHT`), so `x + width <= 320` and `y + height <= 20`. +```java +.position(HudPosition.of(0, 2)) +``` + +`.size(ApolloButtonSize)` the button size, in GUI-scaled pixels. Use one of the suggested chat sizes: +`ChatButton.SIZE_SMALL` (56x16, five fit side by side), `SIZE_MEDIUM` (96x16, three fit side by side) or +`SIZE_ICON` (16x16, for square icon-only buttons) or a fully custom size via `ApolloButtonSize.of(width, height)`. +```java +.size(ChatButton.SIZE_MEDIUM) +.size(ApolloButtonSize.of(56, 16)) +``` + + + + + + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + +**Displaying Chat Buttons** + +```java +public void displayChatButtonsExample(Player viewer) { + ChatButton teamChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("team-chat") + .setPosition(HudPosition.newBuilder().setX(0).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(70).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(0, 0, 0, 128))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(0, 0, 0, 128))) + .setContent(ButtonContent.newBuilder() + .addParts(ButtonContentPart.newBuilder() + .setIcon(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("SHIELD", 0)) + .build()) + .build()) + .addParts(ButtonContentPart.newBuilder() + .setAdventureJsonText(AdventureUtil.toJson(Component.text("Team Chat", NamedTextColor.GREEN))) + .build()) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel team") + .build()) + .build(); + + DisplayChatButtonsMessage message = DisplayChatButtonsMessage.newBuilder() + .addChatButtons(teamChat) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +`run_command`, `open_url` and `client_action` form the button's `on_click`: set at most one; a button without one is purely decorative. `client_action` executes a built-in client action. + +```java +.setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW) +.setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU) +``` + +**Removing a Chat Button** + +```java +public void removeChatButtonExample(Player viewer) { + RemoveChatButtonMessage message = RemoveChatButtonMessage.newBuilder() + .setId("team-chat") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Chat Buttons** + +```java +public void resetChatButtonsExample(Player viewer) { + ResetChatButtonsMessage message = ResetChatButtonsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Updating a Chat Button** + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateChatButtonExample(Player viewer) { + UpdateChatButtonMessage message = UpdateChatButtonMessage.newBuilder() + .setId("public-chat") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(ButtonContentPart.newBuilder() + .setIcon(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("PAPER", 0)) + .build()) + .build()) + .addParts(ButtonContentPart.newBuilder() + .setAdventureJsonText(AdventureUtil.toJson(Component.text("Public Chat", NamedTextColor.AQUA))) + .build()) + .setScale(1.0F) + .build()) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +To keep values live, re-send updates from a repeating task (mirroring the inventory module example) and ideally only to players whose chat is open, +using the serverbound [`PlayerChatOpenMessage`/`PlayerChatCloseMessage`](/apollo/developers/lightweight/protobuf/serverbound-packets) packets from the packet enrichment module: + +```java +Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::broadcastLiveButtonUpdates, 60L, 60L); +``` + + + + + + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + +**Displaying Chat Buttons** + +```java +public void displayChatButtonsExample(Player viewer) { + JsonObject position = new JsonObject(); + position.addProperty("x", 0); + position.addProperty("y", 2); + + JsonObject size = new JsonObject(); + size.addProperty("width", 70); + size.addProperty("height", 16); + + JsonObject button = new JsonObject(); + button.addProperty("id", "team-chat"); + button.add("position", position); + button.add("size", size); + button.addProperty("shape", "BUTTON_SHAPE_ROUNDED_SQUARE"); + button.add("background_color", JsonUtil.createColorObject(new Color(0, 0, 0, 128))); + button.add("border_color", JsonUtil.createColorObject(new Color(0, 0, 0, 128))); + + JsonObject iconPart = new JsonObject(); + iconPart.add("icon", JsonUtil.createItemStackIconObject("SHIELD", 0)); + + JsonObject textPart = new JsonObject(); + textPart.addProperty("adventure_json_text", AdventureUtil.toJson(Component.text("Team Chat", NamedTextColor.GREEN))); + + JsonArray parts = new JsonArray(); + parts.add(iconPart); + parts.add(textPart); + + JsonObject content = new JsonObject(); + content.add("parts", parts); + content.addProperty("scale", 1.0F); + button.add("content", content); + + JsonArray tooltipLines = new JsonArray(); + tooltipLines.add(AdventureUtil.toJson(Component.text("Click to switch!", NamedTextColor.YELLOW))); + + JsonObject tooltip = new JsonObject(); + tooltip.add("adventure_json_lines", tooltipLines); + button.add("tooltip", tooltip); + + button.addProperty("run_command", "/channel team"); + + JsonObject chatButton = new JsonObject(); + chatButton.add("button", button); + + JsonArray buttons = new JsonArray(); + buttons.add(chatButton); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.DisplayChatButtonsMessage"); + message.add("chat_buttons", buttons); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +`run_command`, `open_url` and `client_action` form the button's `on_click`: set at most one; a button without one is purely decorative. `client_action` executes a built-in client action. + +```java +button.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW"); +button.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU"); +``` + +**Removing a Chat Button** + +```java +public void removeChatButtonExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.RemoveChatButtonMessage"); + message.addProperty("id", "team-chat"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Chat Buttons** + +```java +public void resetChatButtonsExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.ResetChatButtonsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Updating a Chat Button** + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateChatButtonExample(Player viewer) { + JsonObject iconPart = new JsonObject(); + iconPart.add("icon", JsonUtil.createItemStackIconObject("PAPER", 0)); + + JsonObject textPart = new JsonObject(); + textPart.addProperty("adventure_json_text", AdventureUtil.toJson(Component.text("Public Chat", NamedTextColor.AQUA))); + + JsonArray parts = new JsonArray(); + parts.add(iconPart); + parts.add(textPart); + + JsonObject content = new JsonObject(); + content.add("parts", parts); + content.addProperty("scale", 1.0F); + + JsonObject update = new JsonObject(); + update.add("content", content); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.UpdateChatButtonMessage"); + message.addProperty("id", "public-chat"); + message.add("update", update); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + + + + + +## Live Button Broadcast + +Chat buttons displayed through the Apollo API with live content parts or a live tooltip are automatically resent while the `buttons.live-broadcast` option is enabled (disabled by default); no +scheduler code required. The broadcast engine ticks every server tick (50ms) and +sends each live piece whenever its own update interval elapses, refreshing content and tooltip independently. + +Every live part and live tooltip carries its own update interval, set through the content builder's +`append(resolver, Duration)` (or `ApolloButtonContentPart.live(resolver, Duration)`) and `ApolloButtonTooltip.live(resolver, Duration)`. +Lightweight integrations replicate this by re-sending `UpdateChatButtonMessage` from their own repeating task. + + + With only `buttons.live-broadcast` enabled, updates are always sent to **every** player holding live + buttons, even while their chat is closed. Also enable the packet enrichment module and its player chat + open/close packets and events. Apollo then only sends updates to players who currently have their chat + open, and pushes fresh values the moment the chat is opened. + + +```diff + modules: + chat: + enable: true + buttons: +- live-broadcast: false ++ live-broadcast: true + packet_enrichment: +- enable: false ++ enable: true + player-chat-open: +- send-packet: false ++ send-packet: true +- fire-apollo-event: false ++ fire-apollo-event: true + player-chat-close: +- send-packet: false ++ send-packet: true +- fire-apollo-event: false ++ fire-apollo-event: true +``` + +## Default Buttons + +Chat buttons can be defined directly in `config.yml` and displayed automatically when a player joins, without any +plugin code. Set `buttons.send-defaults` to `true` (it is `false` by default) and define the buttons under `buttons.defaults`. + +Config buttons are **static**: text is read as legacy strings (`&`-color codes) and icons as icon +definitions. Live content parts and live tooltips require a resolver and are therefore only available +through the API. + +```yaml +modules: + chat: + buttons: + send-defaults: true + defaults: + - id: team-chat + position: + x: 0.0 + y: 2.0 + size: + width: 70.0 + height: 16.0 + shape: ROUNDED_SQUARE # ROUNDED_SQUARE or CIRCLE + background-color: '#80000000' # '#RRGGBB' or '#AARRGGBB' + border-color: '#80000000' + content: + scale: 1.0 + parts: # each part is a 'text' or an 'icon' + - icon: + name: SHIELD + - text: '&aTeam Chat' + tooltip: + - '&eClick to switch!' + on-click: # one of run-command, open-url or client-action + run-command: /channel team +``` + +## Example Layouts + +The example plugin ships two complete, runnable chat button layouts, each implemented in all three integration flavors. +The Apollo API version of both layouts is shown below, the `apollo-protos` and JSON versions are linked underneath each one, and both share a [`ChatButtonParts`](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChatButtonParts.java) helper ([JSON variant](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChatButtonParts.java)) for the pieces every button needs. + +### Channels Layout + +Chat channel switchers: Team, Public and Party. + +![Channels Layout](/modules/chat/channels.png#center) + +```java +public static void display(ChatModule chatModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + ChatButton teamChat = ChatButton.builder() + .id("team-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SHIELD") + .build()) + .append(Component.text("Team Chat", NamedTextColor.GREEN)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel team")) + .build(); + + ChatButton publicChat = ChatButton.builder() + .id("public-chat") + .position(HudPosition.of(76, 2)) + .size(ApolloButtonSize.of(78, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("OAK_SIGN") + .build()) + .append(Component.text("Public Chat")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel public")) + .build(); + + ChatButton partyChat = ChatButton.builder() + .id("party-chat") + .position(HudPosition.of(160, 2)) + .size(ApolloButtonSize.of(76, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FIREWORK_ROCKET") + .build()) + .append(Component.text("Party Chat", NamedTextColor.LIGHT_PURPLE)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel party")) + .build(); + + chatModule.displayChatButtons(apolloPlayer, Arrays.asList(teamChat, publicChat, partyChat)); + }); +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChannelsLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChannelsLayout.java) + +### Staff Chat Layout + +Staff and Public channel switchers plus Clear, Mute and Unmute quick actions. + +![Staff Chat Layout](/modules/chat/staff-chat.png#center) + +```java +public static void display(ChatModule chatModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + ChatButton staffChat = ChatButton.builder() + .id("staff-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMMAND_BLOCK") + .build()) + .append(Component.text("Staff Chat", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel staff")) + .build(); + + ChatButton publicChat = ChatButton.builder() + .id("public-chat") + .position(HudPosition.of(76, 2)) + .size(ApolloButtonSize.of(78, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("OAK_SIGN") + .build()) + .append(Component.text("Public Chat")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel public")) + .build(); + + ChatButton clearChat = ChatButton.builder() + .id("clear-chat") + .position(HudPosition.of(160, 2)) + .size(ApolloButtonSize.of(44, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SPONGE") + .build()) + .append(Component.text("Clear", NamedTextColor.YELLOW)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Clears the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/clearchat")) + .build(); + + ChatButton muteChat = ChatButton.builder() + .id("mute-chat") + .position(HudPosition.of(210, 2)) + .size(ApolloButtonSize.of(44, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("BARRIER") + .build()) + .append(Component.text("Mute", NamedTextColor.RED)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Mutes the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/mutechat")) + .build(); + + ChatButton unmuteChat = ChatButton.builder() + .id("unmute-chat") + .position(HudPosition.of(260, 2)) + .size(ApolloButtonSize.of(56, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("BELL") + .build()) + .append(Component.text("Unmute", NamedTextColor.GREEN)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Unmutes the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/unmutechat")) + .build(); + + chatModule.displayChatButtons(apolloPlayer, Arrays.asList(staffChat, publicChat, + clearChat, muteChat, unmuteChat)); + }); +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/StaffChatLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/StaffChatLayout.java) + +## Available options + +- __`BROADCAST_LIVE_BUTTONS`__ + - Whether live chat button content is automatically re-resolved and re-sent to viewers. + - Values + - Type: `Boolean` + - Default: `false` + +- __`SEND_DEFAULT_BUTTONS`__ + - Whether the default buttons are displayed to players when they join. + - Values + - Type: `Boolean` + - Default: `false` + +- __`DEFAULT_BUTTONS`__ + - The default buttons displayed to joining players while `buttons.send-defaults` is enabled. + - Values + - Type: `List` + - Default: `chat-channels sample buttons` + diff --git a/docs/developers/modules/inventory.mdx b/docs/developers/modules/inventory.mdx index 62b9a65a..fcd486eb 100644 --- a/docs/developers/modules/inventory.mdx +++ b/docs/developers/modules/inventory.mdx @@ -14,6 +14,10 @@ The inventory module allows you to create customizable and professional user int - Ability to copy to clipboard. - Ability to hide item tooltips. - Ability to hide slot highlighting. +- Display clickable inventory buttons in two fixed-size boxes on each side of the player inventory. + - Ability to mix adventure components and icons inside a single button. + - Ability to customize the button shape, size, position, colors and hover colors. + - Ability to run commands or open URLs on click. This module is disabled by default, if you wish to use this module you will need to enable it in `config.yml`. @@ -180,3 +184,1429 @@ public final class ItemUtil { + +## Inventory Buttons + +Display server-driven clickable buttons around the player inventory. Buttons live in two invisible fixed-size +boxes (`92x166` GUI-scaled pixels each) on each side of the player inventory. + +Inventory buttons are built on Apollo's shared button system: `InventoryButton` extends `ApolloButton. +Read the [buttons utilities page](/apollo/developers/utilities/buttons) for the shared +concepts: content, tooltips, click actions, shapes, sizes, colors and the update semantics. + + + Displaying buttons with the same id replaces the previous button. + + +### Sample Code +Explore each integration by cycling through each tab, to find the best fit for your requirements and needs. + + + + + + +**Apollo API examples.** See [General](/apollo/developers/general) for common patterns and helpers. + + +### Displaying Inventory Buttons + +```java +public void displayInventoryExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + InventoryButton shop = InventoryButton.builder() + .id("shop") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("EMERALD") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse categories and buy items", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/shop")) + .build(); + + InventoryButton vote = InventoryButton.builder() + .id("vote") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(34, 204, 68, 64)) + .borderColor(new Color(190, 255, 205, 110)) + .hoveredBackgroundColor(new Color(34, 204, 68, 130)) + .hoveredBorderColor(new Color(190, 255, 205, 210)) + .content(ApolloButtonContent.builder() + .append(Component.text("Vote")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://example.com/vote")) + .build(); + + this.inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(shop, vote)); + }); +} +``` + +### Removing an Inventory Button + +```java +public void removeInventoryButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.inventoryModule.removeInventoryButton(apolloPlayer, "shop"); + this.inventoryModule.removeInventoryButton(apolloPlayer, "vote"); + }); +} +``` + +### Resetting all Inventory Buttons + +```java +public void resetInventoryExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.inventoryModule::resetInventoryButtons); +} +``` + +### Updating an Inventory Button + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateInventoryButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> this.inventoryModule.updateInventoryButton(apolloPlayer, "vote", + ApolloButtonContent.builder() + .append(Component.text("Thanks for voting!", NamedTextColor.GREEN)) + .scale(0.85F) + .build(), + ApolloButtonTooltip.of(Component.text("Come back tomorrow!", NamedTextColor.GRAY)))); +} +``` + +### Live Inventory Buttons + +Content parts and tooltips can be **live**: instead of a static value, you provide a resolver that is called +for each viewing player and re-broadcast automatically by the module (see [Live Button Broadcast](#live-button-broadcast) below). + +```java +public void displayLiveInventoryButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + InventoryButton players = InventoryButton.builder() + .id("players") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> Component.text("Players: ", NamedTextColor.GRAY) + .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)), + Duration.ofMillis(2500L)) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> Arrays.asList( + Component.text("Players currently online", NamedTextColor.GRAY)), Duration.ofMillis(2500L))) + .build(); + + this.inventoryModule.displayInventoryButton(apolloPlayer, players); + }); +} +``` + +### `InventoryButton` Options + +`InventoryButton` extends `ApolloButton`. All shared options: `.id(...)`, `.shape(...)`, `.backgroundColor(...)` & `.borderColor(...)`, +`.content(...)`, `.tooltip(...)`, `.onClick(...)` and the hovered color overrides are documented on the [buttons utilities page](/apollo/developers/utilities/buttons), +along with the `ApolloButtonContent`, `ApolloButtonTooltip` and `ApolloButtonAction` builders used above. + +`.box(InventoryButtonBox)` the box this button is placed in, either `LEFT` or `RIGHT` of the player inventory. +Each box is `92x166` GUI-scaled pixels (`InventoryButton.BOX_WIDTH` x `InventoryButton.BOX_HEIGHT`). +```java +.box(InventoryButtonBox.LEFT) +``` + +`.inventoryType(InventoryType)` the inventory screen this button belongs to. `PLAYER` (the player inventory, +shown on both the survival and creative screens) is the only supported type today. +```java +.inventoryType(InventoryType.PLAYER) +``` + +`.position(HudPosition)` the button position, in GUI-scaled pixels. The button must fit inside the `92x166` box, so `x + width <= 92` and `y + height <= 166`. +```java +.position(HudPosition.of(8, 8)) +``` + +`.size(ApolloButtonSize)` the button size, in GUI-scaled pixels. Use one of the suggested inventory sizes: +`InventoryButton.SIZE_SMALL` (26x26), `SIZE_MEDIUM` (40x40), `SIZE_LARGE` (84x84) or `SIZE_WIDE` (80x26) or +a fully custom size via `ApolloButtonSize.of(width, height)`. +```java +.size(InventoryButton.SIZE_MEDIUM) +.size(ApolloButtonSize.of(56, 24)) +``` + + + + + + +**Lightweight Protobuf examples.** See [Lightweight Protobuf](/apollo/developers/lightweight/protobuf) for setup. + + +**Displaying Inventory Buttons** + +```java +public void displayInventoryExample(Player viewer) { + InventoryButton shop = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("shop") + .setPosition(HudPosition.newBuilder().setX(4).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(255, 255, 255, 40))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(37, 37, 37, 128))) + .setContent(ButtonContent.newBuilder() + .addParts(ButtonContentPart.newBuilder() + .setIcon(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("EMERALD", 0)) + .build()) + .build()) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Shop", NamedTextColor.GREEN))) + .build()) + .setRunCommand("/shop") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + DisplayInventoryButtonsMessage message = DisplayInventoryButtonsMessage.newBuilder() + .addInventoryButtons(shop) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +`run_command`, `open_url` and `client_action` form the button's `on_click`: set at most one; a button without one is purely decorative. `client_action` executes a built-in client action. + +```java +.setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW) +.setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU) +``` + +**Removing an Inventory Button** + +```java +public void removeInventoryButtonExample(Player viewer) { + RemoveInventoryButtonMessage message = RemoveInventoryButtonMessage.newBuilder() + .setId("shop") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Inventory Buttons** + +```java +public void resetInventoryExample(Player viewer) { + ResetInventoryButtonsMessage message = ResetInventoryButtonsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +**Updating an Inventory Button** + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateInventoryButtonExample(Player viewer) { + UpdateInventoryButtonMessage message = UpdateInventoryButtonMessage.newBuilder() + .setId("vote") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(ButtonContentPart.newBuilder() + .setAdventureJsonText(AdventureUtil.toJson( + Component.text("Thanks for voting!", NamedTextColor.GREEN))) + .build()) + .setScale(0.85F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Come back tomorrow!", NamedTextColor.GRAY))) + .build()) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); +} +``` + +To keep values live, re-send updates from a repeating task (mirroring the team module example) and ideally only to players whose inventory is open, +using the serverbound [`PlayerInventoryOpenMessage`/`PlayerInventoryCloseMessage`](/apollo/developers/lightweight/protobuf/serverbound-packets) packets from the packet enrichment module: + +```java +Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::broadcastLiveButtonUpdates, 60L, 60L); +``` + + + + + + +**Lightweight JSON examples.** See [Lightweight JSON](/apollo/developers/lightweight/json) for setup. + + +**Displaying Inventory Buttons** + +```java +public void displayInventoryExample(Player viewer) { + JsonObject position = new JsonObject(); + position.addProperty("x", 4); + position.addProperty("y", 4); + + JsonObject size = new JsonObject(); + size.addProperty("width", 40); + size.addProperty("height", 40); + + JsonObject button = new JsonObject(); + button.addProperty("id", "shop"); + button.add("position", position); + button.add("size", size); + button.addProperty("shape", "BUTTON_SHAPE_ROUNDED_SQUARE"); + button.add("background_color", JsonUtil.createColorObject(new Color(255, 255, 255, 40))); + button.add("border_color", JsonUtil.createColorObject(new Color(37, 37, 37, 128))); + + JsonObject iconPart = new JsonObject(); + iconPart.add("icon", JsonUtil.createItemStackIconObject("EMERALD", 0)); + + JsonArray parts = new JsonArray(); + parts.add(iconPart); + + JsonObject content = new JsonObject(); + content.add("parts", parts); + content.addProperty("scale", 1.0F); + button.add("content", content); + + JsonArray tooltipLines = new JsonArray(); + tooltipLines.add(AdventureUtil.toJson(Component.text("Shop", NamedTextColor.GREEN))); + + JsonObject tooltip = new JsonObject(); + tooltip.add("adventure_json_lines", tooltipLines); + button.add("tooltip", tooltip); + + button.addProperty("run_command", "/shop"); + + JsonObject inventoryButton = new JsonObject(); + inventoryButton.add("button", button); + inventoryButton.addProperty("box", "INVENTORY_BUTTON_BOX_LEFT"); + + JsonArray buttons = new JsonArray(); + buttons.add(inventoryButton); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage"); + message.add("inventory_buttons", buttons); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +`run_command`, `open_url` and `client_action` form the button's `on_click`: set at most one; a button without one is purely decorative. `client_action` executes a built-in client action. + +```java +button.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW"); +button.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU"); +``` + +**Removing an Inventory Button** + +```java +public void removeInventoryButtonExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage"); + message.addProperty("id", "shop"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Resetting all Inventory Buttons** + +```java +public void resetInventoryExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.ResetInventoryButtonsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + +**Updating an Inventory Button** + +Replaces the content and/or tooltip of a previously displayed button, leaving all other button properties unchanged. +Buttons are matched by id; See the [buttons utilities page](/apollo/developers/utilities/buttons#updating-buttons) for the shared update semantics. + +```java +public void updateInventoryButtonExample(Player viewer) { + JsonObject textPart = new JsonObject(); + textPart.addProperty("adventure_json_text", AdventureUtil.toJson( + Component.text("Thanks for voting!", NamedTextColor.GREEN))); + + JsonArray parts = new JsonArray(); + parts.add(textPart); + + JsonObject content = new JsonObject(); + content.add("parts", parts); + content.addProperty("scale", 0.85F); + + JsonArray tooltipLines = new JsonArray(); + tooltipLines.add(AdventureUtil.toJson(Component.text("Come back tomorrow!", NamedTextColor.GRAY))); + + JsonObject tooltip = new JsonObject(); + tooltip.add("adventure_json_lines", tooltipLines); + + JsonObject update = new JsonObject(); + update.add("content", content); + update.add("tooltip", tooltip); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage"); + message.addProperty("id", "vote"); + message.add("update", update); + + JsonPacketUtil.sendPacket(viewer, message); +} +``` + + + + + +## Live Button Broadcast + +Inventory buttons displayed through the Apollo API with live content parts or a live tooltip are automatically resent while the `buttons.live-broadcast` option is enabled (disabled by default); no +scheduler code required. The broadcast engine ticks every server tick (50ms) and +sends each live piece whenever its own update interval elapses, refreshing content and tooltip independently. + +Every live part and live tooltip carries its own update interval, set through the content builder's +`append(resolver, Duration)` (or `ApolloButtonContentPart.live(resolver, Duration)`) and `ApolloButtonTooltip.live(resolver, Duration)`. +Lightweight integrations replicate this by re-sending `UpdateInventoryButtonMessage` from their own repeating task. + + + With only `buttons.live-broadcast` enabled, updates are always sent to **every** player holding live + buttons, even while their inventory is closed. Also enable the packet enrichment module and its player + inventory open/close packets and events. Apollo then only sends updates to players who currently have their + inventory open, and pushes fresh values the moment the inventory is opened. + + +```diff + modules: + inventory: +- enable: false ++ enable: true + buttons: +- live-broadcast: false ++ live-broadcast: true + packet_enrichment: +- enable: false ++ enable: true + player-inventory-open: +- send-packet: false ++ send-packet: true +- fire-apollo-event: false ++ fire-apollo-event: true + player-inventory-close: +- send-packet: false ++ send-packet: true +- fire-apollo-event: false ++ fire-apollo-event: true +``` + +## Default Buttons + +Inventory buttons can be defined directly in `config.yml` and displayed automatically when a player joins, without any +plugin code. Set `buttons.send-defaults` to `true` (it is `false` by default) and define the buttons under `buttons.defaults`. + +Config buttons are **static**: text is read as legacy strings (`&`-color codes) and icons as icon +definitions. Live content parts and live tooltips require a resolver and are therefore only available +through the API. + +```yaml +modules: + inventory: + enable: true + buttons: + send-defaults: true + defaults: + - id: shop + inventory-type: PLAYER # currently only PLAYER + box: LEFT # LEFT or RIGHT + position: + x: 4.0 + y: 4.0 + size: + width: 40.0 + height: 40.0 + shape: ROUNDED_SQUARE # ROUNDED_SQUARE or CIRCLE + background-color: '#28FFFFFF' # '#RRGGBB' or '#AARRGGBB' + border-color: '#80252525' + content: + scale: 1.0 + parts: # each part is a 'text' or an 'icon' + - icon: + name: EMERALD + tooltip: + - '&aShop' + - '&7Browse categories and buy items' + - '&eClick to open' + on-click: # one of run-command, open-url or client-action + run-command: /shop +``` + +## Example Layouts + +The example plugin ships four complete, runnable inventory button layouts, each implemented in all three integration flavors. +The Apollo API version of every layout is shown below, the `apollo-protos` and JSON versions are linked underneath each one, and all share an [`InventoryButtonParts`](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/InventoryButtonParts.java) helper ([JSON variant](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/InventoryButtonParts.java)) for the pieces every button needs. + +### Menu Layout + +A full server menu: shop, spawn, warps, ender chest and a live balance readout, profile, settings, vote, Discord and back to lobby buttons. + +![Menu Layout](/modules/inventory/menu.png#center) + +```java +public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton shop = InventoryButton.builder() + .id("shop") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("EMERALD") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse categories and buy items", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/shop")) + .build(); + + InventoryButton spawn = InventoryButton.builder() + .id("spawn") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("RED_BED") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Spawn", NamedTextColor.AQUA), + Component.text("Teleport back to spawn", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/spawn")) + .build(); + + InventoryButton warps = InventoryButton.builder() + .id("warps") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPASS") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Warps", NamedTextColor.AQUA), + Component.text("Browse public warps", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/warps")) + .build(); + + InventoryButton enderChest = InventoryButton.builder() + .id("enderchest") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("ENDER_CHEST") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE), + Component.text("Open your personal storage", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/enderchest")) + .build(); + + InventoryButton balance = InventoryButton.builder() + .id("balance") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 92)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("GOLD_INGOT") + .build()) + .append(ApolloButtonContentPart.live(apolloViewer -> Component.text("$" + + String.format("%,d", getBalance(apolloViewer.getUniqueId())), NamedTextColor.GOLD), + Duration.ofMillis(2500L))) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Your balance", NamedTextColor.GOLD))) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + InventoryButton vote = InventoryButton.builder() + .id("vote") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(34, 204, 68, 64)) + .borderColor(new Color(190, 255, 205, 110)) + .hoveredBackgroundColor(new Color(34, 204, 68, 130)) + .hoveredBorderColor(new Color(190, 255, 205, 210)) + .content(ApolloButtonContent.builder() + .append(Component.text("Vote")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://example.com/vote")) + .build(); + + InventoryButton discord = InventoryButton.builder() + .id("discord") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 84)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(88, 101, 242, 90)) + .borderColor(new Color(150, 160, 250, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Discord")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Discord", NamedTextColor.BLUE), + Component.text("Join our community", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://lunarclient.dev/discord")) + .build(); + + InventoryButton lobby = InventoryButton.builder() + .id("lobby") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 136)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Back to Lobby")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/lobby")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(shop, spawn, warps, enderChest, balance, + profile, settings, vote, discord, lobby)); + }); +} + +// Demo economy: replace with your economy plugin lookup (e.g. Vault) +private static long getBalance(UUID playerIdentifier) { + long drift = (System.currentTimeMillis() / 10_000L) % 250L; + return 1_000L + Math.abs(playerIdentifier.hashCode() % 4_000) + drift; +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MenuLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MenuLayout.java) + +### Hub Layout + +A hub server selector: Practice, Factions, BedWars and SoupPvP, profile, settings and a changelog button. + +![Hub Layout](/modules/inventory/hub.png#center) + +```java +public static void display(InventoryModule inventoryModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton practice = InventoryButton.builder() + .id("practice") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SPLASH_POTION") + .potion("healing") + .build()) + .append(Component.text("Practice", NamedTextColor.LIGHT_PURPLE)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server practice")) + .build(); + + InventoryButton factions = InventoryButton.builder() + .id("factions") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("TNT") + .build()) + .append(Component.text("Factions", NamedTextColor.RED)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server factions")) + .build(); + + InventoryButton bedWars = InventoryButton.builder() + .id("bedwars") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("RED_BED") + .build()) + .append(Component.text("BedWars", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server bedwars")) + .build(); + + InventoryButton soupPvP = InventoryButton.builder() + .id("souppvp") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("MUSHROOM_STEW") + .build()) + .append(Component.text("SoupPvP", NamedTextColor.GOLD)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server souppvp")) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + InventoryButton changelog = InventoryButton.builder() + .id("changelog") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("WRITABLE_BOOK") + .build()) + .append(Component.text("Changelog")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("πŸŒ™ Apollo - v1.2.8", NamedTextColor.GOLD), + Component.empty(), + Component.text("β€’ Released Markers Module", NamedTextColor.GRAY), + Component.text("β€’ Added ALLOW_DIG_AND_USE & DISABLE_BLOCK_MISS_PENALTY", NamedTextColor.GRAY), + Component.text(" options to Combat Module", NamedTextColor.GRAY), + Component.text("β€’ Added configurable Server Link Button placement", NamedTextColor.GRAY), + Component.text("β€’ Added API option to auto-enable Staff Mods", NamedTextColor.GRAY), + Component.text(" when unlocked via the Staff Mod Module", NamedTextColor.GRAY), + Component.text("β€’ Improved performance with various optimizations", NamedTextColor.GRAY), + Component.empty(), + Component.text("Read the full changelog at", NamedTextColor.YELLOW), + Component.text("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.openUrl("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(practice, factions, bedWars, + soupPvP, profile, settings, changelog)); + }); +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/HubLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/HubLayout.java) + +### Minigame Layout + +A minigame overlay: map info, a live kill counter and mod-aware Show Map / Waypoints buttons using client actions, profile, settings and back to lobby buttons. + +![Minigame Layout](/modules/inventory/minigame.png#center) + +```java +public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton mapInfo = InventoryButton.builder() + .id("map-info") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(Component.text("Map: Apollo", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Built by the Lunar Client Team"), + Component.text("Released in 2024", NamedTextColor.GRAY))) + .build(); + + InventoryButton kills = InventoryButton.builder() + .id("kills") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("IRON_SWORD") + .build()) + .append(apolloViewer -> Component.text("Kills: ", NamedTextColor.GRAY) + .append(Component.text(getKills(apolloViewer), NamedTextColor.RED)), + Duration.ofMillis(2500L)) + .scale(1.0F) + .build()) + .build(); + + InventoryButton lobby = InventoryButton.builder() + .id("lobby") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 136)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Back to Lobby")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/lobby")) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class); + boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED); + boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED); + + InventoryButton.InventoryButtonBuilder showMapBuilder = InventoryButton.builder() + .id("show-map") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FILLED_MAP") + .build()) + .append(Component.text("Show Map")) + .scale(1.0F) + .build()); + + if (minimapEnabled) { + showMapBuilder + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .tooltip(ApolloButtonTooltip.of( + Component.text("Show Map", NamedTextColor.AQUA), + Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW)); + } else { + showMapBuilder + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED))); + } + + InventoryButton showMap = showMapBuilder.build(); + + InventoryButton.InventoryButtonBuilder waypointsBuilder = InventoryButton.builder() + .id("waypoints") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 84)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("LODESTONE") + .build()) + .append(Component.text("Waypoints")) + .scale(1.0F) + .build()); + + if (waypointsEnabled) { + waypointsBuilder + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .tooltip(ApolloButtonTooltip.of( + Component.text("Waypoints", NamedTextColor.GOLD), + Component.text("Manage your waypoints", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU)); + } else { + waypointsBuilder + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED))); + } + + InventoryButton waypoints = waypointsBuilder.build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(mapInfo, kills, lobby, + profile, settings, showMap, waypoints)); + }); +} + +private static int getKills(ApolloPlayer apolloViewer) { + Player player = Bukkit.getPlayer(apolloViewer.getUniqueId()); + return player != null ? player.getStatistic(Statistic.PLAYER_KILLS) : 0; +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MinigameLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MinigameLayout.java) + +### Staff Layout + +A live server-stats dashboard: player count, TPS, CPU and RAM refreshing every second (requires `buttons.live-broadcast`), gamemode switch buttons. + +![Staff Layout](/modules/inventory/staff.png#center) + +```java +private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); + +public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton players = InventoryButton.builder() + .id("players") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> Component.text("Players: ", NamedTextColor.GRAY) + .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)), + Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> Arrays.asList( + Component.text("Players currently online", NamedTextColor.GRAY), + Component.empty(), refreshedLine()), + Duration.ofSeconds(1))) + .build(); + + InventoryButton tps = InventoryButton.builder() + .id("tps") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> tpsContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> tpsTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton cpu = InventoryButton.builder() + .id("cpu") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> cpuContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> cpuTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton ram = InventoryButton.builder() + .id("ram") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> ramContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> ramTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton survival = InventoryButton.builder() + .id("survival") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("IRON_SWORD") + .build()) + .append(Component.text("Survival")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode survival")) + .build(); + + InventoryButton creative = InventoryButton.builder() + .id("creative") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("GRASS_BLOCK") + .build()) + .append(Component.text("Creative")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode creative")) + .build(); + + InventoryButton adventure = InventoryButton.builder() + .id("adventure") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FILLED_MAP") + .build()) + .append(Component.text("Adventure")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode adventure")) + .build(); + + InventoryButton spectator = InventoryButton.builder() + .id("spectator") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("ENDER_EYE") + .build()) + .append(Component.text("Spectator")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode spectator")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(players, tps, cpu, ram, + survival, creative, adventure, spectator)); + }); +} + +private static Component tpsContent() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length == 0) { + return Component.text("TPS: N/A", NamedTextColor.GRAY); + } + + double recent = Math.min(20.0D, tps[0]); + return Component.text("TPS: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.1f", recent), tpsColor(recent))); +} + +private static List tpsTooltip() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length < 3) { + return Arrays.asList( + Component.text("TPS averages require a Paper based server", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + return Arrays.asList( + tpsAverageLine("1m", tps[0]), + tpsAverageLine("5m", tps[1]), + tpsAverageLine("15m", tps[2]), + Component.empty(), + refreshedLine()); +} + +private static Component tpsAverageLine(String window, double average) { + double tps = Math.min(20.0D, average); + return Component.text(window + ": ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", tps), tpsColor(tps))); +} + +private static NamedTextColor tpsColor(double tps) { + if (tps >= 18.0D) { + return NamedTextColor.GREEN; + } + + return tps >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; +} + +private static Component cpuContent() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Component.text("CPU: N/A", NamedTextColor.GRAY); + } + + return Component.text("CPU: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", load), cpuColor(load))); +} + +private static List cpuTooltip() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Arrays.asList( + Component.text("The system load average is unavailable", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + int cores = ServerStatsUtil.getAvailableProcessors(); + double perCore = load * 100.0D / cores; + return Arrays.asList( + Component.text("System load average (last minute)", NamedTextColor.GRAY), + Component.text("Cores: ", NamedTextColor.GRAY) + .append(Component.text(cores, NamedTextColor.WHITE)), + Component.text("Per core: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.0f%%", perCore), cpuColor(load))), + Component.empty(), + refreshedLine()); +} + +private static NamedTextColor cpuColor(double load) { + double perCore = load / ServerStatsUtil.getAvailableProcessors(); + if (perCore < 0.5D) { + return NamedTextColor.GREEN; + } + + return perCore < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; +} + +private static Component ramContent() { + long used = ServerStatsUtil.getUsedRamMb(); + long max = ServerStatsUtil.getMaxRamMb(); + long percent = max <= 0 ? 0 : used * 100 / max; + + return Component.text("RAM: ", NamedTextColor.GRAY) + .append(Component.text(percent + "%", ramColor(percent))); +} + +private static List ramTooltip() { + return Arrays.asList( + Component.text("Used: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getUsedRamMb() + " MB", NamedTextColor.WHITE)), + Component.text("Max: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getMaxRamMb() + " MB", NamedTextColor.WHITE)), + Component.empty(), + refreshedLine()); +} + +private static NamedTextColor ramColor(long percent) { + if (percent < 60) { + return NamedTextColor.GREEN; + } + + return percent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED; +} + +private static Component refreshedLine() { + return Component.text("Updated at " + LocalTime.now().format(TIME_FORMAT), NamedTextColor.YELLOW, TextDecoration.ITALIC); +} +``` + +The same layout built with raw payloads: [apollo-protos implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/StaffLayout.java) Β· [JSON implementation](https://github.com/LunarClient/Apollo/blob/master/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/StaffLayout.java) + +## Available options + +- __`BROADCAST_LIVE_BUTTONS`__ + - Whether live inventory button content is automatically re-resolved and re-sent to viewers. + - Values + - Type: `Boolean` + - Default: `false` + +- __`SEND_DEFAULT_BUTTONS`__ + - Whether the default buttons are displayed to players when they join. + - Values + - Type: `Boolean` + - Default: `false` + +- __`DEFAULT_BUTTONS`__ + - The default buttons displayed to joining players while `buttons.send-defaults` is enabled. + - Values + - Type: `List` + - Default: `menu-style sample buttons` diff --git a/docs/developers/modules/packetenrichment.mdx b/docs/developers/modules/packetenrichment.mdx index 3aba0e1c..728cf986 100644 --- a/docs/developers/modules/packetenrichment.mdx +++ b/docs/developers/modules/packetenrichment.mdx @@ -30,6 +30,8 @@ The majority of this module is handled through Apollo's [event system](/apollo/d The following events are related to the packet enrichment module, you can find more information about each event on the [events page](/apollo/developers/events). * `ApolloPlayerChatCloseEvent` * `ApolloPlayerChatOpenEvent` +* `ApolloPlayerInventoryCloseEvent` +* `ApolloPlayerInventoryOpenEvent` * `ApolloPlayerAttackEvent` * `ApolloPlayerUseItemEvent` * `ApolloPlayerUseItemBucketEvent` @@ -89,6 +91,32 @@ Visit [Apollo Serverbound packets](/apollo/developers/lightweight/protobuf/serve - Type: `Boolean` - Default: `false` +- __`PLAYER_INVENTORY_OPEN_PACKET`__ + - Controls whether the client sends an additional player inventory open packet to the server. + - Values + - Type: `Boolean` + - Default: `false` + +- __`PLAYER_INVENTORY_OPEN_EVENT`__ + - Controls whether Apollo fires `ApolloPlayerInventoryOpenEvent` when the packet is received. + - Disable this and handle the packet yourself if you require asynchronous or off-thread processing. + - Values + - Type: `Boolean` + - Default: `false` + +- __`PLAYER_INVENTORY_CLOSE_PACKET`__ + - Controls whether the client sends an additional player inventory close packet to the server. + - Values + - Type: `Boolean` + - Default: `false` + +- __`PLAYER_INVENTORY_CLOSE_EVENT`__ + - Controls whether Apollo fires `ApolloPlayerInventoryCloseEvent` when the packet is received. + - Disable this and handle the packet yourself if you require asynchronous or off-thread processing. + - Values + - Type: `Boolean` + - Default: `false` + - __`PLAYER_USE_ITEM_PACKET`__ - Controls whether the client sends an additional player use item packet to the server. - Values diff --git a/docs/developers/utilities/_meta.json b/docs/developers/utilities/_meta.json index 9007de2b..8f08d627 100644 --- a/docs/developers/utilities/_meta.json +++ b/docs/developers/utilities/_meta.json @@ -1,4 +1,5 @@ { + "buttons": "Buttons", "colors": "Colors", "cuboids": "Cuboids", "icons": "Icons", diff --git a/docs/developers/utilities/buttons.mdx b/docs/developers/utilities/buttons.mdx new file mode 100644 index 00000000..d85e985a --- /dev/null +++ b/docs/developers/utilities/buttons.mdx @@ -0,0 +1,306 @@ +import { Callout } from 'nextra-theme-docs' + +# Buttons + +## Overview + +Apollo provides a shared button system in the `com.lunarclient.apollo.common.button` package. The abstract +`ApolloButton` carries everything a clickable GUI button needs: content, tooltip, colors, shape, size and a +click action; independently of where the button is displayed. Each *surface* then defines the placement. + +Two surfaces are available today: the [inventory module](/apollo/developers/modules/inventory)'s inventory +buttons, placed in two fixed-size boxes on each side of the player inventory, and the +[chat module](/apollo/developers/modules/chat)'s chat buttons, placed in a strip between the chat input and +the chat log. + +Buttons are always built through a surface type (e.g. `InventoryButton.builder()`), which adds the surface's +placement properties and defines the container the button `position` is measured against. There is no +standalone `ApolloButton.builder()`. + +## `ApolloButton` Properties + +Every surface builder inherits the following properties from `ApolloButton`. + +```java +public abstract class ApolloButton { + + + /** + * Returns the button {@link String} id, unique within a single + * display batch. + * + *

Displaying another button with the same id replaces the + * previous one.

+ * + * @since 1.2.9 + */ + @NotNull String id; + + /** + * Returns the {@link HudPosition} of this button, relative to the + * top-left corner of the container it is placed in. + * + *

The button must fit inside the container: {@code 0 <= x}, + * {@code 0 <= y}, {@code x + width <= container width} and + * {@code y + height <= container height}. See the surface type for + * its container dimensions (e.g. {@code InventoryButton#BOX_WIDTH}).

+ * + * @since 1.2.9 + */ + @NotNull HudPosition position; + + /** + * Returns the {@link ApolloButtonSize} of this button. + * + *

Use one of the surface's suggested sizes (e.g. + * {@code InventoryButton.SIZE_MEDIUM}) or a fully custom + * {@link ApolloButtonSize#of(float, float)}.

+ * + * @return the button size + * @since 1.2.9 + */ + @NotNull ApolloButtonSize size; + + /** + * Returns the {@link ApolloButtonShape} of this button. + * + * @since 1.2.9 + */ + @NotNull ApolloButtonShape shape; + + /** + * Returns the {@link ApolloButtonContent} rendered inside this button. + * + *

Built via {@code ApolloButtonContent.builder()}, appending static + * components, icons or live per-player parts that are re-resolved + * whenever the content is sent (see the owning module's live button + * broadcast option, e.g. {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).

+ * + * @return the button content + * @since 1.2.9 + */ + @NotNull ApolloButtonContent content; + + /** + * Returns the {@link ApolloButtonTooltip} shown while this button is hovered. + * + *

Built via {@code ApolloButtonTooltip.of(...)} for static lines, + * or {@code ApolloButtonTooltip.live(...)} for per-player lines that + * are re-resolved whenever the button content is sent (see the owning + * module's live button broadcast option, e.g. + * {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).

+ * + * @return the tooltip, or {@code null} for no tooltip + * @since 1.2.9 + */ + @Builder.Default + @Nullable ApolloButtonTooltip tooltip = null; + + /** + * Returns the {@link ApolloButtonAction} executed when this button + * is clicked. + * + *

Built via {@link ApolloButtonAction#runCommand(String)}, + * {@link ApolloButtonAction#openUrl(String)} or + * {@link ApolloButtonAction#clientAction(ApolloButtonClientAction)}.

+ * + * @return the click action, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable ApolloButtonAction onClick = null; + + /** + * Returns the background {@link Color} used while this button is hovered. + * + * @return the hovered background color, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable Color hoveredBackgroundColor = null; + + /** + * Returns the border {@link Color} used while this button is hovered. + * + * @return the hovered border color, or {@code null} + * @since 1.2.9 + */ + @Builder.Default + @Nullable Color hoveredBorderColor = null; + + /** + * Returns the background {@link Color} of this button. + * + * @return the background color + * @since 1.2.9 + */ + public abstract Color getBackgroundColor(); + + /** + * Returns the border {@link Color} of this button. + * + * @return the border color + * @since 1.2.9 + */ + public abstract Color getBorderColor(); + +} +``` + +### Sample Code + +The chain below builds an inventory button; `.box(...)` is the inventory surface's placement property, while +everything else is inherited from `ApolloButton`. + +```java +public void displayButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + InventoryButton vote = InventoryButton.builder() + .id("vote") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(Component.text("Vote")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://example.com/vote")) + .build(); + + this.inventoryModule.displayInventoryButton(apolloPlayer, vote); + }); +} +``` + +## `ApolloButtonContent` Builder + +Button content is an ordered sequence of adventure components and icons, rendered as a single centered row. +At least one part is required, and content supports at most 30 parts (`ApolloButtonContent.MAX_PARTS`) + +`.append(Component)` appends an adventure component to the content row. +```java +.append(Component.text("Vote for us!")) +``` + +`.append(Icon)` appends an icon to the content row. Read the [icons utilities page](/apollo/developers/utilities/icons) to learn more about icons. +```java +.append(ItemStackIcon.builder().itemName("NETHER_STAR").build()) +``` + +`.append(Function, Duration)` appends a live part, resolved into a component for each viewing +player whenever the content is sent: at the owning module's live button broadcast (e.g. the inventory module's [live button broadcast](/apollo/developers/modules/inventory#live-button-broadcast)) +and refreshed at the given interval. The interval must be positive and is quantized to server ticks (50ms). +Shorthand for `append(ApolloButtonContentPart.live(resolver, updateInterval))`. +```java +.append(apolloViewer -> Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN), Duration.ofMillis(2500L)) +``` + +`.append(ApolloButtonContentPart)` appends a pre-built content part; see the `ApolloButtonContentPart` factories (`component(...)`, `icon(...)`, `live(...)`). +```java +.append(ApolloButtonContentPart.live( + apolloViewer -> Component.text(getBalance(apolloViewer.getUniqueId()), NamedTextColor.GOLD), + Duration.ofMillis(2500L))) +``` + +`.scale(float)` sets the scale factor applied to the content row. Defaults to `1.0` and must be between `0.25` and `4`. +```java +.scale(1.0F) +``` + +### Sample Code + +```java +public static ApolloButtonContent buttonContentExample() { + return ApolloButtonContent.builder() + .append(ItemStackIcon.builder().itemName("GOLD_INGOT").build()) + .append(Component.text("Balance: ", NamedTextColor.GRAY)) + .append(ApolloButtonContentPart.live( + apolloViewer -> Component.text(getBalance(apolloViewer.getUniqueId()), NamedTextColor.GOLD), + Duration.ofMillis(2500L))) + .scale(1.0F) + .build(); +} +``` + +## `ApolloButtonTooltip` + +The tooltip shown while a button is hovered, rendered like a vanilla item tooltip at the mouse. Built via +`ApolloButtonTooltip.of(...)` for static lines (accepting either varargs or a `List`), or +`ApolloButtonTooltip.live(resolver, Duration)` for a per-player resolver that is re-resolved whenever the +button content is sent, refreshed at the given interval. The interval must be positive and is quantized to +server ticks (50ms). Tooltips support at most 100 lines (`ApolloButtonTooltip.MAX_LINES`). + +```java +ApolloButtonTooltip.of( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse the server shop", NamedTextColor.GRAY)); + +ApolloButtonTooltip.live(apolloViewer -> Arrays.asList( + Component.text("Updated " + LocalTime.now(), NamedTextColor.DARK_GRAY)), + Duration.ofMillis(2500L)); +``` + +## `ApolloButtonAction` + +The action executed when a button is clicked. A button without an action is purely decorative. Built via one +of three factories, each returning a dedicated `ApolloButtonAction` subtype (`RunCommandAction`, `OpenUrlAction`, `ClientAction`): + +- `ApolloButtonAction.runCommand(String)` runs the command as the player. Must start with `/`. The client + shows a confirmation prompt before running the command. +- `ApolloButtonAction.openUrl(String)` opens the URL. Respects the player's chat links setting and Apollo + link-prompt settings. +- `ApolloButtonAction.clientAction(ApolloButtonClientAction)` executes a built-in client action: + `OPEN_MINIMAP_VIEW` opens Lunar's fullscreen minimap view (does nothing when the player has the MiniMap mod + disabled) or `OPEN_WAYPOINTS_MENU` opens the waypoints menu. + +```java +.onClick(ApolloButtonAction.runCommand("/shop")) +.onClick(ApolloButtonAction.openUrl("https://example.com/vote")) +.onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW)) +``` + +## `ApolloButtonShape` + +The button shape, either `ROUNDED_SQUARE` or `CIRCLE`. Rounded squares render with slightly rounded corners; +circles use the smaller of the button's `width` and `height` as their diameter, centered within the button +bounds. + +```java +.shape(ApolloButtonShape.ROUNDED_SQUARE) +``` + +## `ApolloButtonSize` + +The button size, in GUI-scaled pixels, created via `ApolloButtonSize.of(width, height)`. + +```java +.size(ApolloButtonSize.of(56, 24)) +.size(ApolloButtonSize.of(40)) +``` + +Suggested size presets are per-surface, tuned to the surface's container dimensions; for inventory buttons they live +on `InventoryButton` (`SIZE_SMALL`, `SIZE_MEDIUM`, `SIZE_LARGE`, `SIZE_WIDE`) and for chat buttons on `ChatButton` (`SIZE_SMALL`, `SIZE_MEDIUM`, `SIZE_ICON`); +see the [inventory module page](/apollo/developers/modules/inventory#inventorybutton-options) and the [chat module page](/apollo/developers/modules/chat#chat-buttons). + +## Updating Buttons + +Modules expose update methods that replace the content and/or tooltip of a previously displayed button, +leaving all other button properties unchanged (e.g. `InventoryModule#updateInventoryButton`, which takes an +`ApolloButtonContent` and an `ApolloButtonTooltip`). Buttons are matched by id. + +The partial-update semantics are the same for every surface: + +- An unset `content` keeps the previous content; its parts and its scale; a present content replaces both. +- An unset `tooltip` keeps the previous tooltip; a present tooltip fully replaces it, and a present tooltip with zero lines clears it. + +Live content parts and live tooltips are resolved per recipient before sending, both on manual updates and by the owning +module's automatic live button broadcast. The broadcast refreshes content and tooltip independently, each on its own update interval. diff --git a/docs/developers/utilities/icons.mdx b/docs/developers/utilities/icons.mdx index 28eba917..1d29d9e3 100644 --- a/docs/developers/utilities/icons.mdx +++ b/docs/developers/utilities/icons.mdx @@ -9,7 +9,9 @@ Apollo adds three different icon builders, `ItemStackIcon`, `SimpleResourceLocat ## `ItemStackIcon` Builder The `ItemStackIcon` builder is used to assign an icon using a specified ItemStack name or ID. This will utilize the texture present in the player's resource pack as the icon. -If you're using a custom resource pack and want to make the icon appear as a model, you can set the `customModelData` to the models data value. On versions below 1.13 `customModelData` sets the durability instead. +If you're using a custom resource pack and want to make the icon appear as a model, you can set the `customModelDataObject` field to a `CustomModelData` object holding the model's data values. On versions below 1.13 the custom model data sets the durability instead. + +For potion items, set `potion` to a potion id e.g. `ItemStackIcon.builder().itemName("SPLASH_POTION").potion("healing").build()`. If your server accepts players on Minecraft versions higher than `1.8.8` then you need to use `itemName`. @@ -50,6 +52,14 @@ public final class ItemStackIcon extends Icon { */ @Nullable Profile profile; + /** + * Returns the icon {@link String} potion id (e.g. {@code "healing"}). + * + * @return the icon potion id + * @since 1.2.9 + */ + @Nullable String potion; + } ``` diff --git a/docs/public/modules/chat/channels.png b/docs/public/modules/chat/channels.png new file mode 100644 index 0000000000000000000000000000000000000000..0bf3fb3423cbb53e5b0d68f0e4efd305c5bf8713 GIT binary patch literal 5752 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�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 JtIl~Q{vRTZRjmL3 literal 0 HcmV?d00001 diff --git a/docs/public/modules/chat/staff-chat.png b/docs/public/modules/chat/staff-chat.png new file mode 100644 index 0000000000000000000000000000000000000000..cb892dcdd6868ce2d992deb017b6aad94e9cafa7 GIT binary patch literal 9462 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 IH_e~@A05or$^ZZW literal 0 HcmV?d00001 diff --git a/docs/public/modules/inventory/hub.png b/docs/public/modules/inventory/hub.png new file mode 100644 index 0000000000000000000000000000000000000000..8a12da8c476605ca5ad86f28d33c0392860c3a42 GIT binary patch literal 40783 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 literal 0 HcmV?d00001 diff --git a/docs/public/modules/inventory/menu.png b/docs/public/modules/inventory/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..5ad5c178078b8c7f6815e1775eb7a47a9e00fe88 GIT binary patch literal 39266 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 z z|8ahd`@B4dS-e-xDHmsc(_9qFy1f(BSx8W%C{)7bAe%tA@CClBH$P#zNu$nAW5!{` zUVdMShH#xjK z&v&fT^O)&%;~eSQ4b4jIkXNSMboRLWN(6JWs-{~hluv9N+@+*TWtn{*M@83gHj8}2%*^07ar8I-wzqq_>M0MOHKUtG>9Z zSz$7{?%%7j3!#h(0a8WzUu^nwB-(p@5>C_Nf4ork6JE{3vteu)?$l$=7nbXAWWup9 z&MU+zj=?Xgi-egf6*imTuNEKXDoHpd+KE%V1aRdksCf| zwiZ*I9An4Xqb&2{jGBq)aoao|}i<1==#Ju}AaKq%vV^ z=|^!1g#zP(!l0nXbl(!6Lp4|r( z8`_$tv4S-_fsK>_3CPeyjOH&JnnAAAu|}5X`KJ=YJ+{`<>ak@N7vrhNCmH}@<+;en zB`P&T77VqwQ0g`@xG368|7TD3U`lfD6J@k=R@PDYkO~qoP(I z?6HM*4_3uVH(@yG$0{Kho6_d@fyFYyK zY2d26&Q)v}@U9HzeDN!hwZMP^Rbm`xp-hNcH?nL;f7%JIy*b&ivFIbT1ghVCyLh-f zj_&a^&r`*U2`$lVHzjHtP95kk)T4Kh!!k!%6k@sS6ML{n^4eU}YPY?)B1$yZDo6KN z2h{$S70muvt}+MGK{R$wI3nk?V%uPWCT2;*P^uPx0U%|g_NYQAX`8r7C`O(}%}^?< zoFn1al;_Wi{!<2 zsF#Eex{Axr%6!EUs@0UEma;(;qrKS}u(Y6#MVM7}q~i_;e*quS=IGes_z|M?MQ)-JFLh`4bJ=if#&q9p93Tbhe3wn`6b{R(!_zLvnm4r5^Q8lOq*d zn}w2+=5PfdlLaaep~{v8&++Lym>hgx+F;b_!8W-N7x1Tk5Ft#7iP4?_SJO~ZXqJ=* z)P_Bk;LB$fE2?Src=OJ%O3{WwwL|G9+ZTViFG~8yt6< zMAy7Lrs;?UUu=vYbK;3Tmd3__B&9V;Csqb;bNH47C2vq^ux!+NOrwsD?iK4GYAs1m zy?dRUx=~&!y3~W*c5X%O1)b?G8SmK$=ISG~f%w{3I5Qc5O}XvBOr59c{$dxcx zZ8f6WPD$LEOo|gSbqj;NrYbJcND5zCfa`7gsCX4|*oBsOD#4hzH^gIE4Lj5l;P9k{ z(x#$c9ir?VVyYlGDOUfOSH+`fuC_^Ub27lF^90cVY@F29V)9MT2Uri0#!sqnkwj?1 zm~;wc{|1fHLnPzZ>sB8+k$a0WYXM`Yw)+6fno~SCy?Km)?1=wUe^01+KCw)ZKidKp zaa5$1vVV3Qmis>ciO79U4bxcJ{oU;^gN4~V<1t?!n^{uDJ*bRTL8>du)BIF6nC^Md zxAzNnHTGF|&wtfYU=6crxUUkC%K0@8*P_S>!tZ%Szm3niH z#7)XF`^xxt?%psCufZ&9)a)!GwzG`5n|V$y`N(rpjbaDPQ64kc@l|YkYx3`>fs>kZ zVjMEmYjO}BVOIP^xQ-kd2dJd_W$^SjE{ny8Iy+2)r&z3b{EC}6i5J_%@&ZL~o_fA+ zlOh?XU{;OHb5wi7>}(b^D^Kvn0pHO-3|)(5XhT-K;U{j54l1Cu4e~yy8+mB;+v0TsR}3f21nm74B15C?}jPTMXK*fS(Qb<1zRVa>`L@}^FCAVR1C^j zlmVN(c;eRN!L`DoW>#giwHp>968|xXkMnBZ)hA$j6cXc?P%X*Ut|!}@nm^*G#w)xB z4P0`{*cM&9Os4qYlsnQkc2=qabQ$9)aHB#OM>DYE4D|i(nd`bP07YtPbqm-MKF?Bs zQ7Fk7@CkxUXUwwH7I?F?D5f5ZFnWwrY{Cmq;;5+i@wip@gt%t5#ZZ)C+kCZP=-2R` zHs6>=cRA{qGqe2d;=p0aUn7waTk}hLS*zf%X&b)(h^-%4s%Ew9tytP9&6!ci3qg}>lMZedK�zR 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 literal 0 HcmV?d00001 diff --git a/docs/public/modules/inventory/staff.png b/docs/public/modules/inventory/staff.png new file mode 100644 index 0000000000000000000000000000000000000000..241f085a34c0438daaaf3c3d7bcf05ea90f8d3f6 GIT binary patch literal 32947 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 AB>(^b literal 0 HcmV?d00001 diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java index ffdc2396..d9124449 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/ApolloApiExamplePlatform.java @@ -42,6 +42,7 @@ import com.lunarclient.apollo.example.api.module.GlowApiExample; import com.lunarclient.apollo.example.api.module.HeightLimitApiExample; import com.lunarclient.apollo.example.api.module.HologramApiExample; +import com.lunarclient.apollo.example.api.module.InventoryApiExample; import com.lunarclient.apollo.example.api.module.LimbApiExample; import com.lunarclient.apollo.example.api.module.MarkerApiExample; import com.lunarclient.apollo.example.api.module.ModSettingsApiExample; @@ -88,6 +89,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamApiExample()); this.setBorderExample(new BorderApiExample()); this.setChatExample(new ChatApiExample()); + this.setInventoryExample(new InventoryApiExample()); this.setCosmeticExample(new CosmeticApiExample()); this.setColoredFireExample(new ColoredFireApiExample()); this.setCombatExample(new CombatApiExample()); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ChatApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ChatApiExample.java index 13f325a1..e420411a 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ChatApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/ChatApiExample.java @@ -24,15 +24,22 @@ package com.lunarclient.apollo.example.api.module; import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.api.module.chatbuttons.ChannelsLayout; +import com.lunarclient.apollo.example.api.module.chatbuttons.StaffChatLayout; import com.lunarclient.apollo.example.module.impl.ChatExample; import com.lunarclient.apollo.example.util.ServerUtil; import com.lunarclient.apollo.module.chat.ChatModule; +import com.lunarclient.apollo.player.ApolloPlayer; import com.lunarclient.apollo.recipients.Recipients; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; public class ChatApiExample extends ChatExample { @@ -99,6 +106,42 @@ private void runFoliaChatMessageTask() { }, 1L, 20L); } + @Override + public void displayChannelsLayoutExample(Player viewer) { + ChannelsLayout.display(this.chatModule, viewer); + } + + @Override + public void displayStaffChatLayoutExample(Player viewer) { + StaffChatLayout.display(this.chatModule, viewer); + } + + @Override + public void resetChatButtonsExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.chatModule::resetChatButtons); + } + + @Override + public void updateChatButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> this.chatModule.updateChatButtonContent(apolloPlayer, "public-chat", + ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PAPER") + .build()) + .append(Component.text("Public Chat", NamedTextColor.AQUA)) + .scale(1.0F) + .build())); + } + + @Override + public void removeChatButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.chatModule.removeChatButton(apolloPlayer, "party-chat")); + } + @Override public void removeLiveChatMessageExample() { this.chatModule.removeLiveChatMessage(Recipients.ofEveryone(), 13); diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java new file mode 100644 index 00000000..016dd510 --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/InventoryApiExample.java @@ -0,0 +1,141 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.event.ApolloListener; +import com.lunarclient.apollo.event.EventBus; +import com.lunarclient.apollo.event.Listen; +import com.lunarclient.apollo.event.modsetting.ApolloUpdateModOptionEvent; +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.api.module.inventorybuttons.HubLayout; +import com.lunarclient.apollo.example.api.module.inventorybuttons.MenuLayout; +import com.lunarclient.apollo.example.api.module.inventorybuttons.MinigameLayout; +import com.lunarclient.apollo.example.api.module.inventorybuttons.StaffLayout; +import com.lunarclient.apollo.example.module.impl.InventoryExample; +import com.lunarclient.apollo.mods.impl.ModMinimap; +import com.lunarclient.apollo.mods.impl.ModWaypoints; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class InventoryApiExample extends InventoryExample implements ApolloListener, Listener { + + private final InventoryModule inventoryModule = Apollo.getModuleManager().getModule(InventoryModule.class); + + private final Set minigameViewers = new HashSet<>(); + + public InventoryApiExample() { + EventBus.getBus().register(this); + Bukkit.getPluginManager().registerEvents(this, ApolloExamplePlugin.getInstance()); + } + + @Override + public void displayMenuLayoutExample(Player viewer) { + MenuLayout.display(this.inventoryModule, viewer); + } + + @Override + public void displayHubLayoutExample(Player viewer) { + HubLayout.display(this.inventoryModule, viewer); + } + + @Override + public void displayMinigameLayoutExample(Player viewer) { + this.minigameViewers.add(viewer.getUniqueId()); + MinigameLayout.display(this.inventoryModule, viewer); + } + + @Override + public void displayStaffLayoutExample(Player viewer) { + StaffLayout.display(this.inventoryModule, viewer); + } + + @Override + public void removeInventoryButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> { + this.inventoryModule.removeInventoryButton(apolloPlayer, "shop"); + this.inventoryModule.removeInventoryButton(apolloPlayer, "vote"); + }); + } + + @Override + public void updateInventoryButtonExample(Player viewer) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + + apolloPlayerOpt.ifPresent(apolloPlayer -> this.inventoryModule.updateInventoryButton(apolloPlayer, "vote", + ApolloButtonContent.builder() + .append(Component.text("Thanks for voting!", NamedTextColor.GREEN)) + .scale(0.85F) + .build(), + ApolloButtonTooltip.of(Component.text("Come back tomorrow!", NamedTextColor.GRAY)))); + } + + @Override + public void resetInventoryButtonsExample(Player viewer) { + this.minigameViewers.remove(viewer.getUniqueId()); + + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(this.inventoryModule::resetInventoryButtons); + } + + @EventHandler + private void onPlayerQuit(PlayerQuitEvent event) { + this.minigameViewers.remove(event.getPlayer().getUniqueId()); + } + + // Update Minigame layout if the player toggles their Minimap or Waypoint mod + @Listen + private void onApolloUpdateModOption(ApolloUpdateModOptionEvent event) { + String optionKey = event.getOption().getKey(); + if (!ModMinimap.ENABLED.getKey().equals(optionKey) && !ModWaypoints.ENABLED.getKey().equals(optionKey)) { + return; + } + + UUID playerIdentifier = event.getPlayer().getUniqueId(); + if (!this.minigameViewers.contains(playerIdentifier)) { + return; + } + + Player viewer = Bukkit.getPlayer(playerIdentifier); + if (viewer != null) { + MinigameLayout.display(this.inventoryModule, viewer); + } + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/ChannelsLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/ChannelsLayout.java new file mode 100644 index 00000000..816a6608 --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/ChannelsLayout.java @@ -0,0 +1,106 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.chatbuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.module.chat.ChatButton; +import com.lunarclient.apollo.module.chat.ChatModule; +import java.util.Arrays; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class ChannelsLayout { + + public static void display(ChatModule chatModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + ChatButton teamChat = ChatButton.builder() + .id("team-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SHIELD") + .build()) + .append(Component.text("Team Chat", NamedTextColor.GREEN)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel team")) + .build(); + + ChatButton publicChat = ChatButton.builder() + .id("public-chat") + .position(HudPosition.of(76, 2)) + .size(ApolloButtonSize.of(78, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("OAK_SIGN") + .build()) + .append(Component.text("Public Chat")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel public")) + .build(); + + ChatButton partyChat = ChatButton.builder() + .id("party-chat") + .position(HudPosition.of(160, 2)) + .size(ApolloButtonSize.of(76, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FIREWORK_ROCKET") + .build()) + .append(Component.text("Party Chat", NamedTextColor.LIGHT_PURPLE)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel party")) + .build(); + + chatModule.displayChatButtons(apolloPlayer, Arrays.asList(teamChat, publicChat, partyChat)); + }); + } + + private ChannelsLayout() { + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/StaffChatLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/StaffChatLayout.java new file mode 100644 index 00000000..c72a559e --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/chatbuttons/StaffChatLayout.java @@ -0,0 +1,143 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.chatbuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonSize; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.module.chat.ChatButton; +import com.lunarclient.apollo.module.chat.ChatModule; +import java.util.Arrays; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class StaffChatLayout { + + public static void display(ChatModule chatModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + ChatButton staffChat = ChatButton.builder() + .id("staff-chat") + .position(HudPosition.of(0, 2)) + .size(ApolloButtonSize.of(70, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMMAND_BLOCK") + .build()) + .append(Component.text("Staff Chat", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel staff")) + .build(); + + ChatButton publicChat = ChatButton.builder() + .id("public-chat") + .position(HudPosition.of(76, 2)) + .size(ApolloButtonSize.of(78, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("OAK_SIGN") + .build()) + .append(Component.text("Public Chat")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/channel public")) + .build(); + + ChatButton clearChat = ChatButton.builder() + .id("clear-chat") + .position(HudPosition.of(160, 2)) + .size(ApolloButtonSize.of(44, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SPONGE") + .build()) + .append(Component.text("Clear", NamedTextColor.YELLOW)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Clears the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/clearchat")) + .build(); + + ChatButton muteChat = ChatButton.builder() + .id("mute-chat") + .position(HudPosition.of(210, 2)) + .size(ApolloButtonSize.of(44, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("BARRIER") + .build()) + .append(Component.text("Mute", NamedTextColor.RED)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Mutes the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/mutechat")) + .build(); + + ChatButton unmuteChat = ChatButton.builder() + .id("unmute-chat") + .position(HudPosition.of(260, 2)) + .size(ApolloButtonSize.of(56, 16)) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(ChatButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(ChatButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("BELL") + .build()) + .append(Component.text("Unmute", NamedTextColor.GREEN)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Unmutes the public chat", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/unmutechat")) + .build(); + + chatModule.displayChatButtons(apolloPlayer, Arrays.asList(staffChat, publicChat, + clearChat, muteChat, unmuteChat)); + }); + } + + private StaffChatLayout() { + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/HubLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/HubLayout.java new file mode 100644 index 00000000..3cf5e35a --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/HubLayout.java @@ -0,0 +1,217 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.inventorybuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.common.profile.Profile; +import com.lunarclient.apollo.module.inventory.InventoryButton; +import com.lunarclient.apollo.module.inventory.InventoryButtonBox; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryType; +import java.awt.Color; +import java.util.Arrays; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class HubLayout { + + public static void display(InventoryModule inventoryModule, Player viewer) { + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton practice = InventoryButton.builder() + .id("practice") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("SPLASH_POTION") + .potion("healing") + .build()) + .append(Component.text("Practice", NamedTextColor.LIGHT_PURPLE)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server practice")) + .build(); + + InventoryButton factions = InventoryButton.builder() + .id("factions") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("TNT") + .build()) + .append(Component.text("Factions", NamedTextColor.RED)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server factions")) + .build(); + + InventoryButton bedWars = InventoryButton.builder() + .id("bedwars") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("RED_BED") + .build()) + .append(Component.text("BedWars", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server bedwars")) + .build(); + + InventoryButton soupPvP = InventoryButton.builder() + .id("souppvp") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("MUSHROOM_STEW") + .build()) + .append(Component.text("SoupPvP", NamedTextColor.GOLD)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to join!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/server souppvp")) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + InventoryButton changelog = InventoryButton.builder() + .id("changelog") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("WRITABLE_BOOK") + .build()) + .append(Component.text("Changelog")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("πŸŒ™ Apollo - v1.2.8", NamedTextColor.GOLD), + Component.empty(), + Component.text("β€’ Released Markers Module", NamedTextColor.GRAY), + Component.text("β€’ Added ALLOW_DIG_AND_USE & DISABLE_BLOCK_MISS_PENALTY", NamedTextColor.GRAY), + Component.text(" options to Combat Module", NamedTextColor.GRAY), + Component.text("β€’ Added configurable Server Link Button placement", NamedTextColor.GRAY), + Component.text("β€’ Added API option to auto-enable Staff Mods", NamedTextColor.GRAY), + Component.text(" when unlocked via the Staff Mod Module", NamedTextColor.GRAY), + Component.text("β€’ Improved performance with various optimizations", NamedTextColor.GRAY), + Component.empty(), + Component.text("Read the full changelog at", NamedTextColor.YELLOW), + Component.text("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.openUrl("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(practice, factions, bedWars, + soupPvP, profile, settings, changelog)); + }); + } + + private HubLayout() { + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MenuLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MenuLayout.java new file mode 100644 index 00000000..8bb5dace --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MenuLayout.java @@ -0,0 +1,282 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.inventorybuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.common.profile.Profile; +import com.lunarclient.apollo.module.inventory.InventoryButton; +import com.lunarclient.apollo.module.inventory.InventoryButtonBox; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryType; +import java.awt.Color; +import java.time.Duration; +import java.util.Arrays; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class MenuLayout { + + public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton shop = InventoryButton.builder() + .id("shop") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("EMERALD") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse categories and buy items", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/shop")) + .build(); + + InventoryButton spawn = InventoryButton.builder() + .id("spawn") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("RED_BED") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Spawn", NamedTextColor.AQUA), + Component.text("Teleport back to spawn", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/spawn")) + .build(); + + InventoryButton warps = InventoryButton.builder() + .id("warps") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(4, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPASS") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Warps", NamedTextColor.AQUA), + Component.text("Browse public warps", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/warps")) + .build(); + + InventoryButton enderChest = InventoryButton.builder() + .id("enderchest") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(48, 48)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("ENDER_CHEST") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE), + Component.text("Open your personal storage", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/enderchest")) + .build(); + + InventoryButton balance = InventoryButton.builder() + .id("balance") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 92)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("GOLD_INGOT") + .build()) + .append(ApolloButtonContentPart.live(apolloViewer -> Component.text("$" + + String.format("%,d", getBalance(apolloViewer.getUniqueId())), NamedTextColor.GOLD), + Duration.ofMillis(2500L))) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Your balance", NamedTextColor.GOLD))) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + InventoryButton vote = InventoryButton.builder() + .id("vote") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(34, 204, 68, 64)) + .borderColor(new Color(190, 255, 205, 110)) + .hoveredBackgroundColor(new Color(34, 204, 68, 130)) + .hoveredBorderColor(new Color(190, 255, 205, 210)) + .content(ApolloButtonContent.builder() + .append(Component.text("Vote")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://example.com/vote")) + .build(); + + InventoryButton discord = InventoryButton.builder() + .id("discord") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 84)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(88, 101, 242, 90)) + .borderColor(new Color(150, 160, 250, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Discord")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Discord", NamedTextColor.BLUE), + Component.text("Join our community", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.openUrl("https://lunarclient.dev/discord")) + .build(); + + InventoryButton lobby = InventoryButton.builder() + .id("lobby") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 136)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Back to Lobby")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/lobby")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(shop, spawn, warps, enderChest, balance, + profile, settings, vote, discord, lobby)); + }); + } + + // Demo economy: replace with your economy plugin lookup (e.g. Vault) + private static long getBalance(UUID playerIdentifier) { + long drift = (System.currentTimeMillis() / 10_000L) % 250L; + return 1_000L + Math.abs(playerIdentifier.hashCode() % 4_000) + drift; + } + + private MenuLayout() { + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java new file mode 100644 index 00000000..dc1633db --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java @@ -0,0 +1,244 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.inventorybuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.action.ApolloButtonClientAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.common.profile.Profile; +import com.lunarclient.apollo.mods.impl.ModMinimap; +import com.lunarclient.apollo.mods.impl.ModWaypoints; +import com.lunarclient.apollo.module.inventory.InventoryButton; +import com.lunarclient.apollo.module.inventory.InventoryButtonBox; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryType; +import com.lunarclient.apollo.module.modsetting.ModSettingModule; +import com.lunarclient.apollo.player.ApolloPlayer; +import java.awt.Color; +import java.time.Duration; +import java.util.Arrays; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; + +public final class MinigameLayout { + + public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton mapInfo = InventoryButton.builder() + .id("map-info") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(Component.text("Map: Apollo", NamedTextColor.AQUA)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Built by the Lunar Client Team"), + Component.text("Released in 2024", NamedTextColor.GRAY))) + .build(); + + InventoryButton kills = InventoryButton.builder() + .id("kills") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("IRON_SWORD") + .build()) + .append(apolloViewer -> Component.text("Kills: ", NamedTextColor.GRAY) + .append(Component.text(getKills(apolloViewer), NamedTextColor.RED)), + Duration.ofMillis(2500L)) + .scale(1.0F) + .build()) + .build(); + + InventoryButton lobby = InventoryButton.builder() + .id("lobby") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 136)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .content(ApolloButtonContent.builder() + .append(Component.text("Back to Lobby")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/lobby")) + .build(); + + InventoryButton profile = InventoryButton.builder() + .id("profile") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(4, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3 + .profile(Profile.builder() + .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd")) + .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19") + .signature("") + .build()) + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/profile")) + .build(); + + InventoryButton settings = InventoryButton.builder() + .id("settings") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(48, 4)) + .size(InventoryButton.SIZE_MEDIUM) + .shape(ApolloButtonShape.CIRCLE) + .backgroundColor(new Color(222, 160, 60, 85)) + .borderColor(new Color(255, 218, 150, 140)) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("COMPARATOR") + .build()) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.runCommand("/settings")) + .build(); + + ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class); + boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED); + boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED); + + InventoryButton.InventoryButtonBuilder showMapBuilder = InventoryButton.builder() + .id("show-map") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 52)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FILLED_MAP") + .build()) + .append(Component.text("Show Map")) + .scale(1.0F) + .build()); + + if (minimapEnabled) { + showMapBuilder + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .tooltip(ApolloButtonTooltip.of( + Component.text("Show Map", NamedTextColor.AQUA), + Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW)); + } else { + showMapBuilder + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED))); + } + + InventoryButton showMap = showMapBuilder.build(); + + InventoryButton.InventoryButtonBuilder waypointsBuilder = InventoryButton.builder() + .id("waypoints") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 84)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("LODESTONE") + .build()) + .append(Component.text("Waypoints")) + .scale(1.0F) + .build()); + + if (waypointsEnabled) { + waypointsBuilder + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .tooltip(ApolloButtonTooltip.of( + Component.text("Waypoints", NamedTextColor.GOLD), + Component.text("Manage your waypoints", NamedTextColor.GRAY))) + .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU)); + } else { + waypointsBuilder + .backgroundColor(new Color(224, 64, 64, 128)) + .borderColor(new Color(255, 200, 200, 140)) + .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED))); + } + + InventoryButton waypoints = waypointsBuilder.build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(mapInfo, kills, lobby, + profile, settings, showMap, waypoints)); + }); + } + + private static int getKills(ApolloPlayer apolloViewer) { + Player player = Bukkit.getPlayer(apolloViewer.getUniqueId()); + return player != null ? player.getStatistic(Statistic.PLAYER_KILLS) : 0; + } + + private MinigameLayout() { + } + +} diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java new file mode 100644 index 00000000..71922aca --- /dev/null +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java @@ -0,0 +1,327 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.api.module.inventorybuttons; + +import com.lunarclient.apollo.Apollo; +import com.lunarclient.apollo.common.button.ApolloButtonShape; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.action.ApolloButtonAction; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; +import com.lunarclient.apollo.common.icon.ItemStackIcon; +import com.lunarclient.apollo.common.location.HudPosition; +import com.lunarclient.apollo.example.util.ServerStatsUtil; +import com.lunarclient.apollo.module.inventory.InventoryButton; +import com.lunarclient.apollo.module.inventory.InventoryButtonBox; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryType; +import java.time.Duration; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public final class StaffLayout { + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); + + public static void display(InventoryModule inventoryModule, Player viewer) { + // Live parts refresh automatically only while the Apollo config enables + // modules.inventory.buttons.live-broadcast (see the inventory module docs) + Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { + InventoryButton players = InventoryButton.builder() + .id("players") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> Component.text("Players: ", NamedTextColor.GRAY) + .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)), + Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> Arrays.asList( + Component.text("Players currently online", NamedTextColor.GRAY), + Component.empty(), refreshedLine()), + Duration.ofSeconds(1))) + .build(); + + InventoryButton tps = InventoryButton.builder() + .id("tps") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> tpsContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> tpsTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton cpu = InventoryButton.builder() + .id("cpu") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> cpuContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> cpuTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton ram = InventoryButton.builder() + .id("ram") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.LEFT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(apolloViewer -> ramContent(), Duration.ofSeconds(1)) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.live(apolloViewer -> ramTooltip(), Duration.ofSeconds(1))) + .build(); + + InventoryButton survival = InventoryButton.builder() + .id("survival") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 8)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("IRON_SWORD") + .build()) + .append(Component.text("Survival")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode survival")) + .build(); + + InventoryButton creative = InventoryButton.builder() + .id("creative") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 40)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("GRASS_BLOCK") + .build()) + .append(Component.text("Creative")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode creative")) + .build(); + + InventoryButton adventure = InventoryButton.builder() + .id("adventure") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 72)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("FILLED_MAP") + .build()) + .append(Component.text("Adventure")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode adventure")) + .build(); + + InventoryButton spectator = InventoryButton.builder() + .id("spectator") + .inventoryType(InventoryType.PLAYER) + .box(InventoryButtonBox.RIGHT) + .position(HudPosition.of(6, 104)) + .size(InventoryButton.SIZE_WIDE) + .shape(ApolloButtonShape.ROUNDED_SQUARE) + .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR) + .borderColor(InventoryButton.DEFAULT_BORDER_COLOR) + .content(ApolloButtonContent.builder() + .append(ItemStackIcon.builder() + .itemName("ENDER_EYE") + .build()) + .append(Component.text("Spectator")) + .scale(1.0F) + .build()) + .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW))) + .onClick(ApolloButtonAction.runCommand("/gamemode spectator")) + .build(); + + inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(players, tps, cpu, ram, + survival, creative, adventure, spectator)); + }); + } + + private static Component tpsContent() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length == 0) { + return Component.text("TPS: N/A", NamedTextColor.GRAY); + } + + double recent = Math.min(20.0D, tps[0]); + return Component.text("TPS: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.1f", recent), tpsColor(recent))); + } + + private static List tpsTooltip() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length < 3) { + return Arrays.asList( + Component.text("TPS averages require a Paper based server", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + return Arrays.asList( + tpsAverageLine("1m", tps[0]), + tpsAverageLine("5m", tps[1]), + tpsAverageLine("15m", tps[2]), + Component.empty(), + refreshedLine()); + } + + private static Component tpsAverageLine(String window, double average) { + double tps = Math.min(20.0D, average); + return Component.text(window + ": ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", tps), tpsColor(tps))); + } + + private static NamedTextColor tpsColor(double tps) { + if (tps >= 18.0D) { + return NamedTextColor.GREEN; + } + + return tps >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component cpuContent() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Component.text("CPU: N/A", NamedTextColor.GRAY); + } + + return Component.text("CPU: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", load), cpuColor(load))); + } + + private static List cpuTooltip() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Arrays.asList( + Component.text("The system load average is unavailable", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + int cores = ServerStatsUtil.getAvailableProcessors(); + double perCore = load * 100.0D / cores; + return Arrays.asList( + Component.text("System load average (last minute)", NamedTextColor.GRAY), + Component.text("Cores: ", NamedTextColor.GRAY) + .append(Component.text(cores, NamedTextColor.WHITE)), + Component.text("Per core: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.0f%%", perCore), cpuColor(load))), + Component.empty(), + refreshedLine()); + } + + private static NamedTextColor cpuColor(double load) { + double perCore = load / ServerStatsUtil.getAvailableProcessors(); + if (perCore < 0.5D) { + return NamedTextColor.GREEN; + } + + return perCore < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component ramContent() { + long used = ServerStatsUtil.getUsedRamMb(); + long max = ServerStatsUtil.getMaxRamMb(); + long percent = max <= 0 ? 0 : used * 100 / max; + + return Component.text("RAM: ", NamedTextColor.GRAY) + .append(Component.text(percent + "%", ramColor(percent))); + } + + private static List ramTooltip() { + return Arrays.asList( + Component.text("Used: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getUsedRamMb() + " MB", NamedTextColor.WHITE)), + Component.text("Max: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getMaxRamMb() + " MB", NamedTextColor.WHITE)), + Component.empty(), + refreshedLine()); + } + + private static NamedTextColor ramColor(long percent) { + if (percent < 60) { + return NamedTextColor.GREEN; + } + + return percent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component refreshedLine() { + return Component.text("Updated at " + LocalTime.now().format(TIME_FORMAT), NamedTextColor.YELLOW, TextDecoration.ITALIC); + } + + private StaffLayout() { + } + +} 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 433af374..a116a259 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 @@ -211,7 +211,6 @@ private void registerCommonCommands() { private void registerCommonModulesExamples() { this.glintExample = new GlintExample(); - this.inventoryExample = new InventoryExample(); this.saturationExample = new SaturationExample(); } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ChatCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ChatCommand.java index 140060cb..871f0f3b 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ChatCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/ChatCommand.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: /chat "); + this.sendUsage(player); return true; } @@ -62,12 +62,52 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command break; } + case "displaychannels": { + chatExample.displayChannelsLayoutExample(player); + player.sendMessage("Displaying channel chat buttons...."); + break; + } + + case "displaystaff": { + chatExample.displayStaffChatLayoutExample(player); + player.sendMessage("Displaying staff chat buttons...."); + break; + } + + case "updatebutton": { + chatExample.updateChatButtonExample(player); + player.sendMessage("Updating the public chat button...."); + break; + } + + case "removebutton": { + chatExample.removeChatButtonExample(player); + player.sendMessage("Removing the party chat button...."); + break; + } + + case "resetbuttons": { + chatExample.resetChatButtonsExample(player); + player.sendMessage("Resetting chat buttons...."); + break; + } + default: { - player.sendMessage("Usage: /chat "); + this.sendUsage(player); break; } } return true; } + + private void sendUsage(Player player) { + player.sendMessage("Usage: /chat display"); + player.sendMessage("Usage: /chat remove"); + player.sendMessage("Usage: /chat displayChannels"); + player.sendMessage("Usage: /chat displayStaff"); + player.sendMessage("Usage: /chat updateButton"); + player.sendMessage("Usage: /chat removeButton"); + player.sendMessage("Usage: /chat resetButtons"); + } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/InventoryCommand.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/InventoryCommand.java index 9f55e94b..089b1be1 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/InventoryCommand.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/command/InventoryCommand.java @@ -41,14 +41,84 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } Player player = (Player) sender; + + if (args.length == 0) { + this.sendUsage(player); + return true; + } + InventoryExample inventoryExample = ApolloExamplePlugin.getInstance().getInventoryExample(); - if (inventoryExample.inventoryModuleExample(player)) { - player.sendMessage("Giving items..."); - } else { - player.sendMessage("Displaying menu..."); + switch (args[0].toLowerCase()) { + case "giveitems": { + if (inventoryExample.inventoryModuleExample(player)) { + player.sendMessage("Giving items..."); + } else { + player.sendMessage("Displaying menu..."); + } + + break; + } + + case "displaymenu": { + inventoryExample.displayMenuLayoutExample(player); + player.sendMessage("Displaying menu layout buttons..."); + break; + } + + case "displayhub": { + inventoryExample.displayHubLayoutExample(player); + player.sendMessage("Displaying hub layout buttons..."); + break; + } + + case "displayminigame": { + inventoryExample.displayMinigameLayoutExample(player); + player.sendMessage("Displaying minigame layout buttons..."); + break; + } + + case "displaystaff": { + inventoryExample.displayStaffLayoutExample(player); + player.sendMessage("Displaying staff layout buttons..."); + break; + } + + case "removebutton": { + inventoryExample.removeInventoryButtonExample(player); + player.sendMessage("Removing buttons..."); + break; + } + + case "updatebutton": { + inventoryExample.updateInventoryButtonExample(player); + player.sendMessage("Updating the vote button..."); + break; + } + + case "resetbuttons": { + inventoryExample.resetInventoryButtonsExample(player); + player.sendMessage("Resetting buttons..."); + break; + } + + default: { + this.sendUsage(player); + break; + } } return true; } + + private void sendUsage(Player player) { + player.sendMessage("Usage: /inventory giveItems"); + player.sendMessage("Usage: /inventory displayMenu"); + player.sendMessage("Usage: /inventory displayHub"); + player.sendMessage("Usage: /inventory displayMinigame"); + player.sendMessage("Usage: /inventory displayStaff"); + player.sendMessage("Usage: /inventory removeButton"); + player.sendMessage("Usage: /inventory updateButton"); + player.sendMessage("Usage: /inventory resetButtons"); + } } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ChatExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ChatExample.java index 8efd7311..0bc3ee06 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ChatExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/ChatExample.java @@ -24,6 +24,7 @@ package com.lunarclient.apollo.example.module.impl; import com.lunarclient.apollo.example.module.ApolloModuleExample; +import org.bukkit.entity.Player; public abstract class ChatExample extends ApolloModuleExample { @@ -31,4 +32,14 @@ public abstract class ChatExample extends ApolloModuleExample { public abstract void removeLiveChatMessageExample(); + public abstract void displayChannelsLayoutExample(Player viewer); + + public abstract void displayStaffChatLayoutExample(Player viewer); + + public abstract void updateChatButtonExample(Player viewer); + + public abstract void removeChatButtonExample(Player viewer); + + public abstract void resetChatButtonsExample(Player viewer); + } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java index 5d5ed71a..32064984 100644 --- a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/module/impl/InventoryExample.java @@ -32,7 +32,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -public class InventoryExample extends NMSExample { +public abstract class InventoryExample extends NMSExample { public boolean inventoryModuleExample(Player player) { if (this.isOneEight()) { @@ -115,4 +115,18 @@ public void inventoryModuleNMSExample(Player player) { player.openInventory(inventory); } + public abstract void displayMenuLayoutExample(Player viewer); + + public abstract void displayHubLayoutExample(Player viewer); + + public abstract void displayMinigameLayoutExample(Player viewer); + + public abstract void displayStaffLayoutExample(Player viewer); + + public abstract void removeInventoryButtonExample(Player viewer); + + public abstract void updateInventoryButtonExample(Player viewer); + + public abstract void resetInventoryButtonsExample(Player viewer); + } diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/ServerStatsUtil.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/ServerStatsUtil.java new file mode 100644 index 00000000..dbeb4d63 --- /dev/null +++ b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/util/ServerStatsUtil.java @@ -0,0 +1,77 @@ +/* + * 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; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.Method; +import org.bukkit.Bukkit; + +public final class ServerStatsUtil { + + private static final long MB_BYTES = 1024 * 1024; + private static final OperatingSystemMXBean MX_BEAN = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + private static final Method GET_TPS_METHOD = findGetTpsMethod(); + + public static double[] getTps() { + if (GET_TPS_METHOD == null) { + return null; + } + + try { + return (double[]) GET_TPS_METHOD.invoke(Bukkit.getServer()); + } catch (Throwable throwable) { + return null; + } + } + + public static double getSystemLoadAverage() { + return MX_BEAN.getSystemLoadAverage(); + } + + public static int getAvailableProcessors() { + return MX_BEAN.getAvailableProcessors(); + } + + public static long getUsedRamMb() { + Runtime runtime = Runtime.getRuntime(); + return (runtime.totalMemory() - runtime.freeMemory()) / MB_BYTES; + } + + public static long getMaxRamMb() { + return Runtime.getRuntime().maxMemory() / MB_BYTES; + } + + private static Method findGetTpsMethod() { + try { + return Bukkit.getServer() != null ? Bukkit.getServer().getClass().getMethod("getTPS") : null; + } catch (Throwable throwable) { + return null; + } + } + + private ServerStatsUtil() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java index 4ffba9cd..22a99601 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/ApolloJsonExamplePlatform.java @@ -38,6 +38,7 @@ import com.lunarclient.apollo.example.json.module.GlowJsonExample; import com.lunarclient.apollo.example.json.module.HeightLimitJsonExample; import com.lunarclient.apollo.example.json.module.HologramJsonExample; +import com.lunarclient.apollo.example.json.module.InventoryJsonExample; import com.lunarclient.apollo.example.json.module.LimbJsonExample; import com.lunarclient.apollo.example.json.module.MarkerJsonExample; import com.lunarclient.apollo.example.json.module.ModSettingsJsonExample; @@ -76,6 +77,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamJsonExample()); this.setBorderExample(new BorderJsonExample()); this.setChatExample(new ChatJsonExample()); + this.setInventoryExample(new InventoryJsonExample()); this.setCosmeticExample(new CosmeticJsonExample()); this.setColoredFireExample(new ColoredFireJsonExample()); this.setCombatExample(new CombatJsonExample()); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPacketReceiveJsonListener.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPacketReceiveJsonListener.java index 1d8e4656..50401b02 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPacketReceiveJsonListener.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/listener/ApolloPacketReceiveJsonListener.java @@ -27,7 +27,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.json.module.InventoryJsonExample; import com.lunarclient.apollo.example.json.util.JsonUtil; +import com.lunarclient.apollo.example.module.impl.InventoryExample; import java.nio.charset.StandardCharsets; import java.util.UUID; import org.bukkit.Location; @@ -72,6 +74,10 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla this.onPlayerChatOpen(payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerChatCloseMessage".equals(type)) { this.onPlayerChatClose(payload); + } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryOpenMessage".equals(type)) { + this.onPlayerInventoryOpen(player, payload); + } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryCloseMessage".equals(type)) { + this.onPlayerInventoryClose(player, payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemMessage".equals(type)) { this.onPlayerUseItem(payload); } else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemBucketMessage".equals(type)) { @@ -120,6 +126,26 @@ private void onPlayerChatClose(JsonObject message) { this.onPlayerInfo(message.getAsJsonObject("player_info")); } + private void onPlayerInventoryOpen(Player player, JsonObject message) { + long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); + this.onPlayerInfo(message.getAsJsonObject("player_info")); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryJsonExample) { + ((InventoryJsonExample) example).handleInventoryOpen(player); + } + } + + private void onPlayerInventoryClose(Player player, JsonObject message) { + long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); + this.onPlayerInfo(message.getAsJsonObject("player_info")); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryJsonExample) { + ((InventoryJsonExample) example).handleInventoryClose(player); + } + } + private void onPlayerUseItem(JsonObject message) { long instantiationTimeMs = JsonUtil.toJavaTimestamp(message); this.onPlayerInfo(message.getAsJsonObject("player_info")); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ChatJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ChatJsonExample.java index 9c7128f1..f34771c6 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ChatJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/ChatJsonExample.java @@ -25,14 +25,19 @@ import com.google.gson.JsonObject; import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.json.module.chatbuttons.ChannelsLayout; +import com.lunarclient.apollo.example.json.module.chatbuttons.ChatButtonParts; +import com.lunarclient.apollo.example.json.module.chatbuttons.StaffChatLayout; import com.lunarclient.apollo.example.json.util.AdventureUtil; import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; import com.lunarclient.apollo.example.module.impl.ChatExample; import com.lunarclient.apollo.example.util.ServerUtil; import java.util.concurrent.atomic.AtomicInteger; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; public class ChatJsonExample extends ChatExample { @@ -106,6 +111,48 @@ private void runFoliaChatMessageTask() { }, 1L, 20L); } + @Override + public void displayChannelsLayoutExample(Player viewer) { + ChannelsLayout.display(viewer); + } + + @Override + public void displayStaffChatLayoutExample(Player viewer) { + StaffChatLayout.display(viewer); + } + + @Override + public void resetChatButtonsExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.ResetChatButtonsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void updateChatButtonExample(Player viewer) { + JsonObject update = new JsonObject(); + update.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("PAPER", 0)), + ChatButtonParts.textPart(Component.text("Public Chat", NamedTextColor.AQUA)))); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.UpdateChatButtonMessage"); + message.addProperty("id", "public-chat"); + message.add("update", update); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeChatButtonExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.RemoveChatButtonMessage"); + message.addProperty("id", "party-chat"); + + JsonPacketUtil.sendPacket(viewer, message); + } + @Override public void removeLiveChatMessageExample() { JsonObject message = new JsonObject(); diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/InventoryJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/InventoryJsonExample.java new file mode 100644 index 00000000..15bac468 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/InventoryJsonExample.java @@ -0,0 +1,192 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.json.module.inventorybuttons.HubLayout; +import com.lunarclient.apollo.example.json.module.inventorybuttons.InventoryButtonParts; +import com.lunarclient.apollo.example.json.module.inventorybuttons.MenuLayout; +import com.lunarclient.apollo.example.json.module.inventorybuttons.MinigameLayout; +import com.lunarclient.apollo.example.json.module.inventorybuttons.StaffLayout; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.module.impl.InventoryExample; +import com.lunarclient.apollo.example.util.ServerUtil; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class InventoryJsonExample extends InventoryExample implements Listener { + + private final Set staffViewers = ConcurrentHashMap.newKeySet(); + private final Set menuViewers = ConcurrentHashMap.newKeySet(); + private final Set minigameViewers = ConcurrentHashMap.newKeySet(); + + private final Set openInventories = ConcurrentHashMap.newKeySet(); + private volatile boolean inventoryTrackingSeen; + + public InventoryJsonExample() { + if (ServerUtil.isFolia()) { + this.runFoliaLiveButtonTask(); + } else { + this.runBukkitLiveButtonTask(); + } + + Bukkit.getPluginManager().registerEvents(this, ApolloExamplePlugin.getInstance()); + } + + @EventHandler + private void onPlayerQuit(PlayerQuitEvent event) { + UUID playerIdentifier = event.getPlayer().getUniqueId(); + + this.staffViewers.remove(playerIdentifier); + this.menuViewers.remove(playerIdentifier); + this.minigameViewers.remove(playerIdentifier); + this.openInventories.remove(playerIdentifier); + } + + @Override + public void displayMenuLayoutExample(Player viewer) { + MenuLayout.display(viewer); + this.menuViewers.add(viewer.getUniqueId()); + } + + @Override + public void displayHubLayoutExample(Player viewer) { + HubLayout.display(viewer); + } + + @Override + public void displayMinigameLayoutExample(Player viewer) { + MinigameLayout.display(viewer); + this.minigameViewers.add(viewer.getUniqueId()); + } + + @Override + public void displayStaffLayoutExample(Player viewer) { + StaffLayout.display(viewer); + this.staffViewers.add(viewer.getUniqueId()); + } + + @Override + public void removeInventoryButtonExample(Player viewer) { + JsonObject shopMessage = new JsonObject(); + shopMessage.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage"); + shopMessage.addProperty("id", "shop"); + + JsonObject voteMessage = new JsonObject(); + voteMessage.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage"); + voteMessage.addProperty("id", "vote"); + + JsonPacketUtil.sendPacket(viewer, shopMessage); + JsonPacketUtil.sendPacket(viewer, voteMessage); + } + + @Override + public void updateInventoryButtonExample(Player viewer) { + JsonObject update = new JsonObject(); + update.add("content", InventoryButtonParts.createContentObject(0.85F, + InventoryButtonParts.textPart(Component.text("Thanks for voting!", NamedTextColor.GREEN)))); + update.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Come back tomorrow!", NamedTextColor.GRAY))); + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage"); + message.addProperty("id", "vote"); + message.add("update", update); + + JsonPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetInventoryButtonsExample(Player viewer) { + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.ResetInventoryButtonsMessage"); + + JsonPacketUtil.sendPacket(viewer, message); + + UUID playerIdentifier = viewer.getUniqueId(); + this.staffViewers.remove(playerIdentifier); + this.menuViewers.remove(playerIdentifier); + this.minigameViewers.remove(playerIdentifier); + } + + public void handleInventoryOpen(Player player) { + this.inventoryTrackingSeen = true; + this.openInventories.add(player.getUniqueId()); + this.sendLiveUpdates(player); + } + + public void handleInventoryClose(Player player) { + this.inventoryTrackingSeen = true; + this.openInventories.remove(player.getUniqueId()); + } + + private void runBukkitLiveButtonTask() { + Bukkit.getScheduler().runTaskTimerAsynchronously(ApolloExamplePlugin.getInstance(), + this::broadcastLiveButtonUpdates, 50L, 50L); + } + + private void runFoliaLiveButtonTask() { + Bukkit.getAsyncScheduler().runAtFixedRate(ApolloExamplePlugin.getInstance(), + task -> this.broadcastLiveButtonUpdates(), 2500L, 2500L, TimeUnit.MILLISECONDS); + } + + private void broadcastLiveButtonUpdates() { + for (Player player : Bukkit.getOnlinePlayers()) { + UUID playerIdentifier = player.getUniqueId(); + + if (this.inventoryTrackingSeen && !this.openInventories.contains(playerIdentifier)) { + continue; + } + + this.sendLiveUpdates(player); + } + } + + private void sendLiveUpdates(Player player) { + UUID playerIdentifier = player.getUniqueId(); + + if (this.staffViewers.contains(playerIdentifier)) { + StaffLayout.sendUpdates(player); + } + + if (this.menuViewers.contains(playerIdentifier)) { + MenuLayout.sendBalanceUpdate(player); + } + + if (this.minigameViewers.contains(playerIdentifier)) { + MinigameLayout.sendKillsUpdate(player); + } + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChannelsLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChannelsLayout.java new file mode 100644 index 00000000..bbe87a8e --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChannelsLayout.java @@ -0,0 +1,72 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.chatbuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class ChannelsLayout { + + public static void display(Player viewer) { + JsonObject teamChat = ChatButtonParts.createButtonObject("team-chat", 0, 2, 70, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject teamChatButton = ChatButtonParts.button(teamChat); + teamChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("SHIELD", 0)), + ChatButtonParts.textPart(Component.text("Team Chat", NamedTextColor.GREEN)))); + teamChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + teamChatButton.addProperty("run_command", "/channel team"); + + JsonObject publicChat = ChatButtonParts.createButtonObject("public-chat", 76, 2, 78, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject publicChatButton = ChatButtonParts.button(publicChat); + publicChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("OAK_SIGN", 0)), + ChatButtonParts.textPart(Component.text("Public Chat")))); + publicChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + publicChatButton.addProperty("run_command", "/channel public"); + + JsonObject partyChat = ChatButtonParts.createButtonObject("party-chat", 160, 2, 76, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject partyChatButton = ChatButtonParts.button(partyChat); + partyChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("FIREWORK_ROCKET", 0)), + ChatButtonParts.textPart(Component.text("Party Chat", NamedTextColor.LIGHT_PURPLE)))); + partyChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + partyChatButton.addProperty("run_command", "/channel party"); + + JsonPacketUtil.sendPacket(viewer, ChatButtonParts.createDisplayMessage(teamChat, publicChat, partyChat)); + } + + private ChannelsLayout() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChatButtonParts.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChatButtonParts.java new file mode 100644 index 00000000..0a9ea131 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/ChatButtonParts.java @@ -0,0 +1,121 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.chatbuttons; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.AdventureUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import java.awt.Color; +import net.kyori.adventure.text.Component; + +public final class ChatButtonParts { + + public static final Color BACKGROUND = new Color(0, 0, 0, 128); + public static final Color BORDER = new Color(0, 0, 0, 128); + + public static JsonObject createDisplayMessage(JsonObject... buttons) { + JsonArray buttonsArray = new JsonArray(); + for (JsonObject button : buttons) { + buttonsArray.add(button); + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.chat.v1.DisplayChatButtonsMessage"); + message.add("chat_buttons", buttonsArray); + + return message; + } + + public static JsonObject createButtonObject(String id, float x, float y, float width, float height, + String shape, Color backgroundColor, Color borderColor) { + JsonObject position = new JsonObject(); + position.addProperty("x", x); + position.addProperty("y", y); + + JsonObject size = new JsonObject(); + size.addProperty("width", width); + size.addProperty("height", height); + + JsonObject button = new JsonObject(); + button.addProperty("id", id); + button.add("position", position); + button.add("size", size); + button.addProperty("shape", shape); + button.add("background_color", JsonUtil.createColorObject(backgroundColor)); + button.add("border_color", JsonUtil.createColorObject(borderColor)); + + JsonObject wrapper = new JsonObject(); + wrapper.add("button", button); + + return wrapper; + } + + public static JsonObject button(JsonObject wrapper) { + return wrapper.getAsJsonObject("button"); + } + + public static JsonObject createContentObject(float scale, JsonObject... parts) { + JsonArray partsArray = new JsonArray(); + for (JsonObject part : parts) { + partsArray.add(part); + } + + JsonObject content = new JsonObject(); + content.add("parts", partsArray); + content.addProperty("scale", scale); + + return content; + } + + public static JsonObject createTooltipObject(Component... lines) { + JsonArray linesArray = new JsonArray(); + for (Component line : lines) { + linesArray.add(AdventureUtil.toJson(line)); + } + + JsonObject tooltip = new JsonObject(); + tooltip.add("adventure_json_lines", linesArray); + + return tooltip; + } + + public static JsonObject textPart(Component component) { + JsonObject part = new JsonObject(); + part.addProperty("adventure_json_text", AdventureUtil.toJson(component)); + + return part; + } + + public static JsonObject iconPart(JsonObject icon) { + JsonObject part = new JsonObject(); + part.add("icon", icon); + + return part; + } + + private ChatButtonParts() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/StaffChatLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/StaffChatLayout.java new file mode 100644 index 00000000..47960192 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/chatbuttons/StaffChatLayout.java @@ -0,0 +1,93 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.chatbuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class StaffChatLayout { + + public static void display(Player viewer) { + JsonObject staffChat = ChatButtonParts.createButtonObject("staff-chat", 0, 2, 70, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject staffChatButton = ChatButtonParts.button(staffChat); + staffChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("COMMAND_BLOCK", 0)), + ChatButtonParts.textPart(Component.text("Staff Chat", NamedTextColor.AQUA)))); + staffChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + staffChatButton.addProperty("run_command", "/channel staff"); + + JsonObject publicChat = ChatButtonParts.createButtonObject("public-chat", 76, 2, 78, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject publicChatButton = ChatButtonParts.button(publicChat); + publicChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("OAK_SIGN", 0)), + ChatButtonParts.textPart(Component.text("Public Chat")))); + publicChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + publicChatButton.addProperty("run_command", "/channel public"); + + JsonObject clearChat = ChatButtonParts.createButtonObject("clear-chat", 160, 2, 44, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject clearChatButton = ChatButtonParts.button(clearChat); + clearChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("SPONGE", 0)), + ChatButtonParts.textPart(Component.text("Clear", NamedTextColor.YELLOW)))); + clearChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Clears the public chat", NamedTextColor.GRAY))); + clearChatButton.addProperty("run_command", "/clearchat"); + + JsonObject muteChat = ChatButtonParts.createButtonObject("mute-chat", 210, 2, 44, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject muteChatButton = ChatButtonParts.button(muteChat); + muteChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("BARRIER", 0)), + ChatButtonParts.textPart(Component.text("Mute", NamedTextColor.RED)))); + muteChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Mutes the public chat", NamedTextColor.GRAY))); + muteChatButton.addProperty("run_command", "/mutechat"); + + JsonObject unmuteChat = ChatButtonParts.createButtonObject("unmute-chat", 260, 2, 56, 16, + "BUTTON_SHAPE_ROUNDED_SQUARE", ChatButtonParts.BACKGROUND, ChatButtonParts.BORDER); + JsonObject unmuteChatButton = ChatButtonParts.button(unmuteChat); + unmuteChatButton.add("content", ChatButtonParts.createContentObject(1.0F, + ChatButtonParts.iconPart(JsonUtil.createItemStackIconObject("BELL", 0)), + ChatButtonParts.textPart(Component.text("Unmute", NamedTextColor.GREEN)))); + unmuteChatButton.add("tooltip", ChatButtonParts.createTooltipObject( + Component.text("Unmutes the public chat", NamedTextColor.GRAY))); + unmuteChatButton.addProperty("run_command", "/unmutechat"); + + JsonPacketUtil.sendPacket(viewer, ChatButtonParts.createDisplayMessage( + staffChat, publicChat, clearChat, muteChat, unmuteChat)); + } + + private StaffChatLayout() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/HubLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/HubLayout.java new file mode 100644 index 00000000..4fbe9f0b --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/HubLayout.java @@ -0,0 +1,142 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.inventorybuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import java.awt.Color; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class HubLayout { + + public static void display(Player viewer) { + JsonObject practice = InventoryButtonParts.createButtonObject("practice", + "INVENTORY_BUTTON_BOX_LEFT", 6, 8, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject practiceButton = InventoryButtonParts.button(practice); + JsonObject practiceIcon = JsonUtil.createItemStackIconObject("SPLASH_POTION", 0); + practiceIcon.getAsJsonObject("item_stack").addProperty("potion", "healing"); + practiceButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(practiceIcon), + InventoryButtonParts.textPart(Component.text("Practice", NamedTextColor.LIGHT_PURPLE)))); + practiceButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to join!", NamedTextColor.YELLOW))); + practiceButton.addProperty("run_command", "/server practice"); + + JsonObject factions = InventoryButtonParts.createButtonObject("factions", + "INVENTORY_BUTTON_BOX_LEFT", 6, 40, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject factionsButton = InventoryButtonParts.button(factions); + factionsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("TNT", 0)), + InventoryButtonParts.textPart(Component.text("Factions", NamedTextColor.RED)))); + factionsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to join!", NamedTextColor.YELLOW))); + factionsButton.addProperty("run_command", "/server factions"); + + JsonObject bedWars = InventoryButtonParts.createButtonObject("bedwars", + "INVENTORY_BUTTON_BOX_LEFT", 6, 72, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject bedWarsButton = InventoryButtonParts.button(bedWars); + bedWarsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("RED_BED", 0)), + InventoryButtonParts.textPart(Component.text("BedWars", NamedTextColor.AQUA)))); + bedWarsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to join!", NamedTextColor.YELLOW))); + bedWarsButton.addProperty("run_command", "/server bedwars"); + + JsonObject soupPvP = InventoryButtonParts.createButtonObject("souppvp", + "INVENTORY_BUTTON_BOX_LEFT", 6, 104, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject soupPvPButton = InventoryButtonParts.button(soupPvP); + soupPvPButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("MUSHROOM_STEW", 0)), + InventoryButtonParts.textPart(Component.text("SoupPvP", NamedTextColor.GOLD)))); + soupPvPButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to join!", NamedTextColor.YELLOW))); + soupPvPButton.addProperty("run_command", "/server souppvp"); + + JsonObject profile = InventoryButtonParts.createButtonObject("profile", + "INVENTORY_BUTTON_BOX_RIGHT", 4, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject profileButton = InventoryButtonParts.button(profile); + profileButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + JsonUtil.createProfileObject( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )))); + profileButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))); + profileButton.addProperty("run_command", "/profile"); + + JsonObject settings = InventoryButtonParts.createButtonObject("settings", + "INVENTORY_BUTTON_BOX_RIGHT", 48, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", new Color(222, 160, 60, 85), new Color(255, 218, 150, 140)); + JsonObject settingsButton = InventoryButtonParts.button(settings); + settingsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("COMPARATOR", 0)))); + settingsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))); + settingsButton.addProperty("run_command", "/settings"); + + JsonObject changelog = InventoryButtonParts.createButtonObject("changelog", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 52, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject changelogButton = InventoryButtonParts.button(changelog); + changelogButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("WRITABLE_BOOK", 0)), + InventoryButtonParts.textPart(Component.text("Changelog")))); + changelogButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("πŸŒ™ Apollo - v1.2.8", NamedTextColor.GOLD), + Component.empty(), + Component.text("β€’ Released Markers Module", NamedTextColor.GRAY), + Component.text("β€’ Added ALLOW_DIG_AND_USE & DISABLE_BLOCK_MISS_PENALTY", NamedTextColor.GRAY), + Component.text(" options to Combat Module", NamedTextColor.GRAY), + Component.text("β€’ Added configurable Server Link Button placement", NamedTextColor.GRAY), + Component.text("β€’ Added API option to auto-enable Staff Mods", NamedTextColor.GRAY), + Component.text(" when unlocked via the Staff Mod Module", NamedTextColor.GRAY), + Component.text("β€’ Improved performance with various optimizations", NamedTextColor.GRAY), + Component.empty(), + Component.text("Read the full changelog at", NamedTextColor.YELLOW), + Component.text("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8", NamedTextColor.YELLOW))); + changelogButton.addProperty("open_url", "https://github.com/LunarClient/Apollo/releases/tag/v1.2.8"); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createDisplayMessage(practice, factions, + bedWars, soupPvP, profile, settings, changelog)); + } + + private HubLayout() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/InventoryButtonParts.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/InventoryButtonParts.java new file mode 100644 index 00000000..8c7aca8c --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/InventoryButtonParts.java @@ -0,0 +1,137 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.inventorybuttons; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.AdventureUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import java.awt.Color; +import net.kyori.adventure.text.Component; + +public final class InventoryButtonParts { + + public static final Color BACKGROUND = new Color(255, 255, 255, 40); + public static final Color BORDER = new Color(37, 37, 37, 128); + + public static JsonObject createDisplayMessage(JsonObject... buttons) { + JsonArray buttonsArray = new JsonArray(); + for (JsonObject button : buttons) { + buttonsArray.add(button); + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage"); + message.add("inventory_buttons", buttonsArray); + + return message; + } + + public static JsonObject createUpdateMessage(String id, JsonObject content, JsonObject tooltip) { + JsonObject update = new JsonObject(); + update.add("content", content); + if (tooltip != null) { + update.add("tooltip", tooltip); + } + + JsonObject message = new JsonObject(); + message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage"); + message.addProperty("id", id); + message.add("update", update); + + return message; + } + + public static JsonObject createButtonObject(String id, String box, float x, float y, float width, float height, + String shape, Color backgroundColor, Color borderColor) { + JsonObject position = new JsonObject(); + position.addProperty("x", x); + position.addProperty("y", y); + + JsonObject size = new JsonObject(); + size.addProperty("width", width); + size.addProperty("height", height); + + JsonObject button = new JsonObject(); + button.addProperty("id", id); + button.add("position", position); + button.add("size", size); + button.addProperty("shape", shape); + button.add("background_color", JsonUtil.createColorObject(backgroundColor)); + button.add("border_color", JsonUtil.createColorObject(borderColor)); + + JsonObject wrapper = new JsonObject(); + wrapper.add("button", button); + wrapper.addProperty("box", box); + + return wrapper; + } + + public static JsonObject button(JsonObject wrapper) { + return wrapper.getAsJsonObject("button"); + } + + public static JsonObject createContentObject(float scale, JsonObject... parts) { + JsonArray partsArray = new JsonArray(); + for (JsonObject part : parts) { + partsArray.add(part); + } + + JsonObject content = new JsonObject(); + content.add("parts", partsArray); + content.addProperty("scale", scale); + + return content; + } + + public static JsonObject createTooltipObject(Component... lines) { + JsonArray linesArray = new JsonArray(); + for (Component line : lines) { + linesArray.add(AdventureUtil.toJson(line)); + } + + JsonObject tooltip = new JsonObject(); + tooltip.add("adventure_json_lines", linesArray); + + return tooltip; + } + + public static JsonObject textPart(Component component) { + JsonObject part = new JsonObject(); + part.addProperty("adventure_json_text", AdventureUtil.toJson(component)); + + return part; + } + + public static JsonObject iconPart(JsonObject icon) { + JsonObject part = new JsonObject(); + part.add("icon", icon); + + return part; + } + + private InventoryButtonParts() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MenuLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MenuLayout.java new file mode 100644 index 00000000..03c6b061 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MenuLayout.java @@ -0,0 +1,187 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.inventorybuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import java.awt.Color; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class MenuLayout { + + public static void display(Player viewer) { + JsonObject shop = InventoryButtonParts.createButtonObject("shop", + "INVENTORY_BUTTON_BOX_LEFT", 4, 4, 40, 40, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + shop.addProperty("inventory_type", "INVENTORY_TYPE_PLAYER"); + JsonObject shopButton = InventoryButtonParts.button(shop); + shopButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("EMERALD", 0)))); + shopButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Shop", NamedTextColor.GREEN), + Component.text("Browse categories and buy items", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))); + shopButton.addProperty("run_command", "/shop"); + + JsonObject spawn = InventoryButtonParts.createButtonObject("spawn", + "INVENTORY_BUTTON_BOX_LEFT", 48, 4, 40, 40, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject spawnButton = InventoryButtonParts.button(spawn); + spawnButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("RED_BED", 0)))); + spawnButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Spawn", NamedTextColor.AQUA), + Component.text("Teleport back to spawn", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))); + spawnButton.addProperty("run_command", "/spawn"); + + JsonObject warps = InventoryButtonParts.createButtonObject("warps", + "INVENTORY_BUTTON_BOX_LEFT", 4, 48, 40, 40, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject warpsButton = InventoryButtonParts.button(warps); + warpsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("COMPASS", 0)))); + warpsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Warps", NamedTextColor.AQUA), + Component.text("Browse public warps", NamedTextColor.GRAY), + Component.text("Click to teleport", NamedTextColor.YELLOW))); + warpsButton.addProperty("run_command", "/warps"); + + JsonObject enderChest = InventoryButtonParts.createButtonObject("enderchest", + "INVENTORY_BUTTON_BOX_LEFT", 48, 48, 40, 40, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject enderChestButton = InventoryButtonParts.button(enderChest); + enderChestButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("ENDER_CHEST", 0)))); + enderChestButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE), + Component.text("Open your personal storage", NamedTextColor.GRAY), + Component.text("Click to open", NamedTextColor.YELLOW))); + enderChestButton.addProperty("run_command", "/enderchest"); + + JsonObject balance = InventoryButtonParts.createButtonObject("balance", + "INVENTORY_BUTTON_BOX_LEFT", 6, 92, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject balanceButton = InventoryButtonParts.button(balance); + balanceButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("GOLD_INGOT", 0)), + InventoryButtonParts.textPart(balanceContent(viewer)))); + balanceButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Your balance", NamedTextColor.GOLD))); + + JsonObject profile = InventoryButtonParts.createButtonObject("profile", + "INVENTORY_BUTTON_BOX_RIGHT", 4, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject profileButton = InventoryButtonParts.button(profile); + profileButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + JsonUtil.createProfileObject( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )))); + profileButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))); + profileButton.addProperty("run_command", "/profile"); + + JsonObject settings = InventoryButtonParts.createButtonObject("settings", + "INVENTORY_BUTTON_BOX_RIGHT", 48, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", new Color(222, 160, 60, 85), new Color(255, 218, 150, 140)); + JsonObject settingsButton = InventoryButtonParts.button(settings); + settingsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("COMPARATOR", 0)))); + settingsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))); + settingsButton.addProperty("run_command", "/settings"); + + JsonObject vote = InventoryButtonParts.createButtonObject("vote", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 52, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", new Color(34, 204, 68, 64), new Color(190, 255, 205, 110)); + JsonObject voteButton = InventoryButtonParts.button(vote); + voteButton.add("hovered_background_color", JsonUtil.createColorObject(new Color(34, 204, 68, 130))); + voteButton.add("hovered_border_color", JsonUtil.createColorObject(new Color(190, 255, 205, 210))); + voteButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(Component.text("Vote")))); + voteButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Vote", NamedTextColor.GREEN), + Component.text("Vote daily for rewards", NamedTextColor.GRAY))); + voteButton.addProperty("open_url", "https://example.com/vote"); + + JsonObject discord = InventoryButtonParts.createButtonObject("discord", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 84, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", new Color(88, 101, 242, 90), new Color(150, 160, 250, 140)); + JsonObject discordButton = InventoryButtonParts.button(discord); + discordButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(Component.text("Discord")))); + discordButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Discord", NamedTextColor.BLUE), + Component.text("Join our community", NamedTextColor.GRAY))); + discordButton.addProperty("open_url", "https://lunarclient.dev/discord"); + + JsonObject lobby = InventoryButtonParts.createButtonObject("lobby", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 136, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", new Color(224, 64, 64, 128), new Color(255, 200, 200, 140)); + JsonObject lobbyButton = InventoryButtonParts.button(lobby); + lobbyButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(Component.text("Back to Lobby")))); + lobbyButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))); + lobbyButton.addProperty("run_command", "/lobby"); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createDisplayMessage( + shop, spawn, warps, enderChest, balance, profile, settings, vote, discord, lobby)); + } + + public static void sendBalanceUpdate(Player viewer) { + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("balance", + InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("GOLD_INGOT", 0)), + InventoryButtonParts.textPart(balanceContent(viewer))), + InventoryButtonParts.createTooltipObject( + Component.text("Your balance", NamedTextColor.GOLD)))); + } + + private static Component balanceContent(Player viewer) { + return Component.text("$" + String.format("%,d", getBalance(viewer.getUniqueId())), NamedTextColor.GOLD); + } + + // Demo economy: replace with your economy plugin lookup (e.g. Vault) + private static long getBalance(UUID playerIdentifier) { + long drift = (System.currentTimeMillis() / 10_000L) % 250L; + return 1_000L + Math.abs(playerIdentifier.hashCode() % 4_000) + drift; + } + + private MenuLayout() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MinigameLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MinigameLayout.java new file mode 100644 index 00000000..caa17ff0 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/MinigameLayout.java @@ -0,0 +1,142 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.inventorybuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import java.awt.Color; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; + +public final class MinigameLayout { + + public static void display(Player viewer) { + JsonObject mapInfo = InventoryButtonParts.createButtonObject("map-info", + "INVENTORY_BUTTON_BOX_LEFT", 6, 8, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject mapInfoButton = InventoryButtonParts.button(mapInfo); + mapInfoButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(Component.text("Map: Apollo", NamedTextColor.AQUA)))); + mapInfoButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Built by the Lunar Client Team"), + Component.text("Released in 2024", NamedTextColor.GRAY))); + + JsonObject kills = InventoryButtonParts.createButtonObject("kills", + "INVENTORY_BUTTON_BOX_LEFT", 6, 40, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject killsButton = InventoryButtonParts.button(kills); + killsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("IRON_SWORD", 0)), + InventoryButtonParts.textPart(killsContent(viewer)))); + + JsonObject lobby = InventoryButtonParts.createButtonObject("lobby", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 136, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", + new Color(224, 64, 64, 128), new Color(255, 200, 200, 140)); + JsonObject lobbyButton = InventoryButtonParts.button(lobby); + lobbyButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(Component.text("Back to Lobby")))); + lobbyButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Back to Lobby", NamedTextColor.RED), + Component.text("Return to the main lobby", NamedTextColor.GRAY))); + lobbyButton.addProperty("run_command", "/lobby"); + + JsonObject profile = InventoryButtonParts.createButtonObject("profile", + "INVENTORY_BUTTON_BOX_RIGHT", 4, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject profileButton = InventoryButtonParts.button(profile); + profileButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + JsonUtil.createProfileObject( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )))); + profileButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text(viewer.getName(), NamedTextColor.GOLD), + Component.text("View your stats", NamedTextColor.GRAY))); + profileButton.addProperty("run_command", "/profile"); + + JsonObject settings = InventoryButtonParts.createButtonObject("settings", + "INVENTORY_BUTTON_BOX_RIGHT", 48, 4, 40, 40, + "BUTTON_SHAPE_CIRCLE", new Color(222, 160, 60, 85), new Color(255, 218, 150, 140)); + JsonObject settingsButton = InventoryButtonParts.button(settings); + settingsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("COMPARATOR", 0)))); + settingsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Settings", NamedTextColor.WHITE), + Component.text("Server preferences", NamedTextColor.GRAY))); + settingsButton.addProperty("run_command", "/settings"); + + JsonObject showMap = InventoryButtonParts.createButtonObject("show-map", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 52, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject showMapButton = InventoryButtonParts.button(showMap); + showMapButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("FILLED_MAP", 0)), + InventoryButtonParts.textPart(Component.text("Show Map")))); + showMapButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Show Map", NamedTextColor.AQUA), + Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY))); + showMapButton.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW"); + + JsonObject waypoints = InventoryButtonParts.createButtonObject("waypoints", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 84, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject waypointsButton = InventoryButtonParts.button(waypoints); + waypointsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("LODESTONE", 0)), + InventoryButtonParts.textPart(Component.text("Waypoints")))); + waypointsButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Waypoints", NamedTextColor.GOLD), + Component.text("Manage your waypoints", NamedTextColor.GRAY))); + waypointsButton.addProperty("client_action", "BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU"); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createDisplayMessage(mapInfo, kills, + lobby, profile, settings, showMap, waypoints)); + } + + public static void sendKillsUpdate(Player viewer) { + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("kills", + InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("IRON_SWORD", 0)), + InventoryButtonParts.textPart(killsContent(viewer))), + null)); + } + + private static Component killsContent(Player viewer) { + return Component.text("Kills: ", NamedTextColor.GRAY) + .append(Component.text(viewer.getStatistic(Statistic.PLAYER_KILLS), NamedTextColor.RED)); + } + + private MinigameLayout() { + } + +} diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/StaffLayout.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/StaffLayout.java new file mode 100644 index 00000000..106be888 --- /dev/null +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/inventorybuttons/StaffLayout.java @@ -0,0 +1,255 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.json.module.inventorybuttons; + +import com.google.gson.JsonObject; +import com.lunarclient.apollo.example.json.util.JsonPacketUtil; +import com.lunarclient.apollo.example.json.util.JsonUtil; +import com.lunarclient.apollo.example.util.ServerStatsUtil; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public final class StaffLayout { + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); + + public static void display(Player viewer) { + JsonObject players = InventoryButtonParts.createButtonObject("players", + "INVENTORY_BUTTON_BOX_LEFT", 6, 8, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject playersButton = InventoryButtonParts.button(players); + playersButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(playersContent()))); + playersButton.add("tooltip", playersTooltipObject()); + + JsonObject tps = InventoryButtonParts.createButtonObject("tps", + "INVENTORY_BUTTON_BOX_LEFT", 6, 40, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject tpsButton = InventoryButtonParts.button(tps); + tpsButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(tpsContent()))); + tpsButton.add("tooltip", tpsTooltipObject()); + + JsonObject cpu = InventoryButtonParts.createButtonObject("cpu", + "INVENTORY_BUTTON_BOX_LEFT", 6, 72, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject cpuButton = InventoryButtonParts.button(cpu); + cpuButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(cpuContent()))); + cpuButton.add("tooltip", cpuTooltipObject()); + + JsonObject ram = InventoryButtonParts.createButtonObject("ram", + "INVENTORY_BUTTON_BOX_LEFT", 6, 104, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject ramButton = InventoryButtonParts.button(ram); + ramButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.textPart(ramContent()))); + ramButton.add("tooltip", ramTooltipObject()); + + JsonObject survival = InventoryButtonParts.createButtonObject("survival", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 8, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject survivalButton = InventoryButtonParts.button(survival); + survivalButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("IRON_SWORD", 0)), + InventoryButtonParts.textPart(Component.text("Survival")))); + survivalButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + survivalButton.addProperty("run_command", "/gamemode survival"); + + JsonObject creative = InventoryButtonParts.createButtonObject("creative", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 40, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject creativeButton = InventoryButtonParts.button(creative); + creativeButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("GRASS_BLOCK", 0)), + InventoryButtonParts.textPart(Component.text("Creative")))); + creativeButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + creativeButton.addProperty("run_command", "/gamemode creative"); + + JsonObject adventure = InventoryButtonParts.createButtonObject("adventure", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 72, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject adventureButton = InventoryButtonParts.button(adventure); + adventureButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("FILLED_MAP", 0)), + InventoryButtonParts.textPart(Component.text("Adventure")))); + adventureButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + adventureButton.addProperty("run_command", "/gamemode adventure"); + + JsonObject spectator = InventoryButtonParts.createButtonObject("spectator", + "INVENTORY_BUTTON_BOX_RIGHT", 6, 104, 80, 26, + "BUTTON_SHAPE_ROUNDED_SQUARE", InventoryButtonParts.BACKGROUND, InventoryButtonParts.BORDER); + JsonObject spectatorButton = InventoryButtonParts.button(spectator); + spectatorButton.add("content", InventoryButtonParts.createContentObject(1.0F, + InventoryButtonParts.iconPart(JsonUtil.createItemStackIconObject("ENDER_EYE", 0)), + InventoryButtonParts.textPart(Component.text("Spectator")))); + spectatorButton.add("tooltip", InventoryButtonParts.createTooltipObject( + Component.text("Click to switch!", NamedTextColor.YELLOW))); + spectatorButton.addProperty("run_command", "/gamemode spectator"); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createDisplayMessage( + players, tps, cpu, ram, survival, creative, adventure, spectator)); + } + + public static void sendUpdates(Player viewer) { + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("players", + InventoryButtonParts.createContentObject(1.0F, InventoryButtonParts.textPart(playersContent())), + playersTooltipObject())); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("tps", + InventoryButtonParts.createContentObject(1.0F, InventoryButtonParts.textPart(tpsContent())), + tpsTooltipObject())); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("cpu", + InventoryButtonParts.createContentObject(1.0F, InventoryButtonParts.textPart(cpuContent())), + cpuTooltipObject())); + + JsonPacketUtil.sendPacket(viewer, InventoryButtonParts.createUpdateMessage("ram", + InventoryButtonParts.createContentObject(1.0F, InventoryButtonParts.textPart(ramContent())), + ramTooltipObject())); + } + + private static Component playersContent() { + return Component.text("Players: ", NamedTextColor.GRAY) + .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)); + } + + private static JsonObject playersTooltipObject() { + return InventoryButtonParts.createTooltipObject( + Component.text("Players currently online", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + private static JsonObject ramTooltipObject() { + return InventoryButtonParts.createTooltipObject( + Component.text("Used: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getUsedRamMb() + " MB", NamedTextColor.WHITE)), + Component.text("Max: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getMaxRamMb() + " MB", NamedTextColor.WHITE)), + Component.empty(), + refreshedLine()); + } + + private static Component tpsContent() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length == 0) { + return Component.text("TPS: N/A", NamedTextColor.GRAY); + } + + double recent = Math.min(20.0D, tps[0]); + NamedTextColor color = recent >= 18.0D ? NamedTextColor.GREEN + : recent >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + return Component.text("TPS: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.1f", recent), color)); + } + + private static Component cpuContent() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Component.text("CPU: N/A", NamedTextColor.GRAY); + } + + double perCore = load / ServerStatsUtil.getAvailableProcessors(); + NamedTextColor color = perCore < 0.5D ? NamedTextColor.GREEN + : perCore < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + return Component.text("CPU: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", load), color)); + } + + private static Component ramContent() { + long used = ServerStatsUtil.getUsedRamMb(); + long max = ServerStatsUtil.getMaxRamMb(); + long percent = max <= 0 ? 0 : used * 100 / max; + + NamedTextColor color = percent < 60 ? NamedTextColor.GREEN + : percent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED; + return Component.text("RAM: ", NamedTextColor.GRAY) + .append(Component.text(percent + "%", color)); + } + + private static JsonObject tpsTooltipObject() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length < 3) { + return InventoryButtonParts.createTooltipObject( + Component.text("TPS averages require a Paper based server", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + return InventoryButtonParts.createTooltipObject( + tpsAverageLine("1m", tps[0]), + tpsAverageLine("5m", tps[1]), + tpsAverageLine("15m", tps[2]), + Component.empty(), + refreshedLine()); + } + + private static Component tpsAverageLine(String window, double average) { + double tps = Math.min(20.0D, average); + NamedTextColor color = tps >= 18.0D ? NamedTextColor.GREEN + : tps >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + return Component.text(window + ": ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", tps), color)); + } + + private static JsonObject cpuTooltipObject() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return InventoryButtonParts.createTooltipObject( + Component.text("The system load average is unavailable", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + int cores = ServerStatsUtil.getAvailableProcessors(); + double perCore = load * 100.0D / cores; + NamedTextColor color = load / cores < 0.5D ? NamedTextColor.GREEN + : load / cores < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + return InventoryButtonParts.createTooltipObject( + Component.text("System load average (last minute)", NamedTextColor.GRAY), + Component.text("Cores: ", NamedTextColor.GRAY) + .append(Component.text(cores, NamedTextColor.WHITE)), + Component.text("Per core: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.0f%%", perCore), color)), + Component.empty(), + refreshedLine()); + } + + private static Component refreshedLine() { + return Component.text("Updated at " + LocalTime.now().format(TIME_FORMAT), NamedTextColor.YELLOW, TextDecoration.ITALIC); + } + + private StaffLayout() { + } + +} 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 31617883..59733a24 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 @@ -58,6 +58,8 @@ public final class JsonPacketUtil { CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-attack.send-packet", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-open.send-packet", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-close.send-packet", false); + CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-inventory-open.send-packet", false); + CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-inventory-close.send-packet", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-use-item.send-packet", false); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-use-item-bucket.send-packet", false); CONFIG_MODULE_PROPERTIES.put("server_link", "legacy-button-placement", "NEW_ROW"); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java index 5c0b2a5c..680a71e2 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/ApolloProtoExamplePlatform.java @@ -38,6 +38,7 @@ import com.lunarclient.apollo.example.proto.module.GlowProtoExample; import com.lunarclient.apollo.example.proto.module.HeightLimitProtoExample; import com.lunarclient.apollo.example.proto.module.HologramProtoExample; +import com.lunarclient.apollo.example.proto.module.InventoryProtoExample; import com.lunarclient.apollo.example.proto.module.LimbProtoExample; import com.lunarclient.apollo.example.proto.module.MarkerProtoExample; import com.lunarclient.apollo.example.proto.module.ModSettingsProtoExample; @@ -76,6 +77,7 @@ public void registerModuleExamples() { this.setBeamExample(new BeamProtoExample()); this.setBorderExample(new BorderProtoExample()); this.setChatExample(new ChatProtoExample()); + this.setInventoryExample(new InventoryProtoExample()); this.setCosmeticExample(new CosmeticProtoExample()); this.setColoredFireExample(new ColoredFireProtoExample()); this.setCombatExample(new CombatProtoExample()); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPacketReceiveProtoListener.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPacketReceiveProtoListener.java index 63933bd7..2c691b6b 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPacketReceiveProtoListener.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/listener/ApolloPacketReceiveProtoListener.java @@ -31,6 +31,8 @@ import com.lunarclient.apollo.common.v1.LunarClientVersion; import com.lunarclient.apollo.common.v1.MinecraftVersion; import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.module.impl.InventoryExample; +import com.lunarclient.apollo.example.proto.module.InventoryProtoExample; import com.lunarclient.apollo.example.proto.util.ProtobufUtil; import com.lunarclient.apollo.packetenrichment.v1.BlockHit; import com.lunarclient.apollo.packetenrichment.v1.Direction; @@ -39,6 +41,8 @@ import com.lunarclient.apollo.packetenrichment.v1.PlayerChatCloseMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerChatOpenMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerInfo; +import com.lunarclient.apollo.packetenrichment.v1.PlayerInventoryCloseMessage; +import com.lunarclient.apollo.packetenrichment.v1.PlayerInventoryOpenMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerUseItemBucketMessage; import com.lunarclient.apollo.packetenrichment.v1.PlayerUseItemMessage; import com.lunarclient.apollo.packetenrichment.v1.RayTraceResult; @@ -73,6 +77,10 @@ public void onPluginMessageReceived(@NonNull String channel, @NonNull Player pla this.onPlayerChatOpen(any.unpack(PlayerChatOpenMessage.class)); } else if (any.is(PlayerChatCloseMessage.class)) { this.onPlayerChatClose(any.unpack(PlayerChatCloseMessage.class)); + } else if (any.is(PlayerInventoryOpenMessage.class)) { + this.onPlayerInventoryOpen(player, any.unpack(PlayerInventoryOpenMessage.class)); + } else if (any.is(PlayerInventoryCloseMessage.class)) { + this.onPlayerInventoryClose(player, any.unpack(PlayerInventoryCloseMessage.class)); } else if (any.is(PlayerUseItemMessage.class)) { this.onPlayerUseItem(any.unpack(PlayerUseItemMessage.class)); } else if (any.is(PlayerUseItemBucketMessage.class)) { @@ -125,6 +133,30 @@ private void onPlayerChatClose(PlayerChatCloseMessage message) { this.onPlayerInfo(playerInfo); } + private void onPlayerInventoryOpen(Player player, PlayerInventoryOpenMessage message) { + long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); + + PlayerInfo playerInfo = message.getPlayerInfo(); + this.onPlayerInfo(playerInfo); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryProtoExample) { + ((InventoryProtoExample) example).handleInventoryOpen(player); + } + } + + private void onPlayerInventoryClose(Player player, PlayerInventoryCloseMessage message) { + long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); + + PlayerInfo playerInfo = message.getPlayerInfo(); + this.onPlayerInfo(playerInfo); + + InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample(); + if (example instanceof InventoryProtoExample) { + ((InventoryProtoExample) example).handleInventoryClose(player); + } + } + private void onPlayerUseItem(PlayerUseItemMessage message) { long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime()); diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ChatProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ChatProtoExample.java index 48ca3438..02eafe9a 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ChatProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/ChatProtoExample.java @@ -23,17 +23,28 @@ */ package com.lunarclient.apollo.example.proto.module; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonUpdate; import com.lunarclient.apollo.chat.v1.DisplayLiveChatMessageMessage; +import com.lunarclient.apollo.chat.v1.RemoveChatButtonMessage; import com.lunarclient.apollo.chat.v1.RemoveLiveChatMessageMessage; +import com.lunarclient.apollo.chat.v1.ResetChatButtonsMessage; +import com.lunarclient.apollo.chat.v1.UpdateChatButtonMessage; +import com.lunarclient.apollo.common.v1.Icon; import com.lunarclient.apollo.example.ApolloExamplePlugin; import com.lunarclient.apollo.example.module.impl.ChatExample; +import com.lunarclient.apollo.example.proto.module.chatbuttons.ChannelsLayout; +import com.lunarclient.apollo.example.proto.module.chatbuttons.ChatButtonParts; +import com.lunarclient.apollo.example.proto.module.chatbuttons.StaffChatLayout; import com.lunarclient.apollo.example.proto.util.AdventureUtil; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; import com.lunarclient.apollo.example.util.ServerUtil; import java.util.concurrent.atomic.AtomicInteger; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; public class ChatProtoExample extends ChatExample { @@ -105,6 +116,51 @@ private void runFoliaChatMessageTask() { }, 1L, 20L); } + @Override + public void displayChannelsLayoutExample(Player viewer) { + ChannelsLayout.display(viewer); + } + + @Override + public void displayStaffChatLayoutExample(Player viewer) { + StaffChatLayout.display(viewer); + } + + @Override + public void resetChatButtonsExample(Player viewer) { + ResetChatButtonsMessage message = ResetChatButtonsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void updateChatButtonExample(Player viewer) { + ButtonUpdate update = ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("PAPER", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Public Chat", NamedTextColor.AQUA))) + .setScale(1.0F) + .build()) + .build(); + + UpdateChatButtonMessage message = UpdateChatButtonMessage.newBuilder() + .setId("public-chat") + .setUpdate(update) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void removeChatButtonExample(Player viewer) { + RemoveChatButtonMessage message = RemoveChatButtonMessage.newBuilder() + .setId("party-chat") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + @Override public void removeLiveChatMessageExample() { RemoveLiveChatMessageMessage message = RemoveLiveChatMessageMessage.newBuilder() diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/InventoryProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/InventoryProtoExample.java new file mode 100644 index 00000000..b4b1746a --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/InventoryProtoExample.java @@ -0,0 +1,200 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module; + +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.example.ApolloExamplePlugin; +import com.lunarclient.apollo.example.module.impl.InventoryExample; +import com.lunarclient.apollo.example.proto.module.inventorybuttons.HubLayout; +import com.lunarclient.apollo.example.proto.module.inventorybuttons.InventoryButtonParts; +import com.lunarclient.apollo.example.proto.module.inventorybuttons.MenuLayout; +import com.lunarclient.apollo.example.proto.module.inventorybuttons.MinigameLayout; +import com.lunarclient.apollo.example.proto.module.inventorybuttons.StaffLayout; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.util.ServerUtil; +import com.lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage; +import com.lunarclient.apollo.inventory.v1.ResetInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class InventoryProtoExample extends InventoryExample implements Listener { + + private final Set staffViewers = ConcurrentHashMap.newKeySet(); + private final Set menuViewers = ConcurrentHashMap.newKeySet(); + private final Set minigameViewers = ConcurrentHashMap.newKeySet(); + + private final Set openInventories = ConcurrentHashMap.newKeySet(); + private volatile boolean inventoryTrackingSeen; + + public InventoryProtoExample() { + if (ServerUtil.isFolia()) { + this.runFoliaLiveButtonTask(); + } else { + this.runBukkitLiveButtonTask(); + } + + Bukkit.getPluginManager().registerEvents(this, ApolloExamplePlugin.getInstance()); + } + + @EventHandler + private void onPlayerQuit(PlayerQuitEvent event) { + UUID playerIdentifier = event.getPlayer().getUniqueId(); + + this.staffViewers.remove(playerIdentifier); + this.menuViewers.remove(playerIdentifier); + this.minigameViewers.remove(playerIdentifier); + this.openInventories.remove(playerIdentifier); + } + + @Override + public void displayMenuLayoutExample(Player viewer) { + MenuLayout.display(viewer); + this.menuViewers.add(viewer.getUniqueId()); + } + + @Override + public void displayHubLayoutExample(Player viewer) { + HubLayout.display(viewer); + } + + @Override + public void displayMinigameLayoutExample(Player viewer) { + MinigameLayout.display(viewer); + this.minigameViewers.add(viewer.getUniqueId()); + } + + @Override + public void displayStaffLayoutExample(Player viewer) { + StaffLayout.display(viewer); + this.staffViewers.add(viewer.getUniqueId()); + } + + @Override + public void removeInventoryButtonExample(Player viewer) { + RemoveInventoryButtonMessage shopMessage = RemoveInventoryButtonMessage.newBuilder() + .setId("shop") + .build(); + + RemoveInventoryButtonMessage voteMessage = RemoveInventoryButtonMessage.newBuilder() + .setId("vote") + .build(); + + ProtobufPacketUtil.sendPacket(viewer, shopMessage); + ProtobufPacketUtil.sendPacket(viewer, voteMessage); + } + + @Override + public void updateInventoryButtonExample(Player viewer) { + ButtonUpdate update = ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Thanks for voting!", NamedTextColor.GREEN))) + .setScale(0.85F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Come back tomorrow!", NamedTextColor.GRAY))) + .build()) + .build(); + + UpdateInventoryButtonMessage message = UpdateInventoryButtonMessage.newBuilder() + .setId("vote") + .setUpdate(update) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + @Override + public void resetInventoryButtonsExample(Player viewer) { + ResetInventoryButtonsMessage message = ResetInventoryButtonsMessage.getDefaultInstance(); + ProtobufPacketUtil.sendPacket(viewer, message); + + UUID playerIdentifier = viewer.getUniqueId(); + this.staffViewers.remove(playerIdentifier); + this.menuViewers.remove(playerIdentifier); + this.minigameViewers.remove(playerIdentifier); + } + + public void handleInventoryOpen(Player player) { + this.inventoryTrackingSeen = true; + this.openInventories.add(player.getUniqueId()); + this.sendLiveUpdates(player); + } + + public void handleInventoryClose(Player player) { + this.inventoryTrackingSeen = true; + this.openInventories.remove(player.getUniqueId()); + } + + private void runBukkitLiveButtonTask() { + Bukkit.getScheduler().runTaskTimerAsynchronously(ApolloExamplePlugin.getInstance(), + this::broadcastLiveButtonUpdates, 50L, 50L); + } + + private void runFoliaLiveButtonTask() { + Bukkit.getAsyncScheduler().runAtFixedRate(ApolloExamplePlugin.getInstance(), + task -> this.broadcastLiveButtonUpdates(), 2500L, 2500L, TimeUnit.MILLISECONDS); + } + + private void broadcastLiveButtonUpdates() { + for (Player player : Bukkit.getOnlinePlayers()) { + UUID playerIdentifier = player.getUniqueId(); + + if (this.inventoryTrackingSeen && !this.openInventories.contains(playerIdentifier)) { + continue; + } + + this.sendLiveUpdates(player); + } + } + + private void sendLiveUpdates(Player player) { + UUID playerIdentifier = player.getUniqueId(); + + if (this.staffViewers.contains(playerIdentifier)) { + StaffLayout.sendUpdates(player); + } + + if (this.menuViewers.contains(playerIdentifier)) { + MenuLayout.sendBalanceUpdate(player); + } + + if (this.minigameViewers.contains(playerIdentifier)) { + MinigameLayout.sendKillsUpdate(player); + } + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChannelsLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChannelsLayout.java new file mode 100644 index 00000000..d2a7e114 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChannelsLayout.java @@ -0,0 +1,126 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.chatbuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.chat.v1.ChatButton; +import com.lunarclient.apollo.chat.v1.DisplayChatButtonsMessage; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class ChannelsLayout { + + public static void display(Player viewer) { + ChatButton teamChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("team-chat") + .setPosition(HudPosition.newBuilder().setX(0).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(70).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("SHIELD", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Team Chat", NamedTextColor.GREEN))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel team") + .build()) + .build(); + + ChatButton publicChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("public-chat") + .setPosition(HudPosition.newBuilder().setX(76).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(78).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("OAK_SIGN", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Public Chat"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel public") + .build()) + .build(); + + ChatButton partyChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("party-chat") + .setPosition(HudPosition.newBuilder().setX(160).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(76).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("FIREWORK_ROCKET", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Party Chat", NamedTextColor.LIGHT_PURPLE))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel party") + .build()) + .build(); + + DisplayChatButtonsMessage message = DisplayChatButtonsMessage.newBuilder() + .addChatButtons(teamChat) + .addChatButtons(publicChat) + .addChatButtons(partyChat) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + private ChannelsLayout() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChatButtonParts.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChatButtonParts.java new file mode 100644 index 00000000..8b268687 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/ChatButtonParts.java @@ -0,0 +1,52 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.chatbuttons; + +import com.lunarclient.apollo.button.v1.ButtonContentPart; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import java.awt.Color; +import net.kyori.adventure.text.Component; + +public final class ChatButtonParts { + + public static final Color BACKGROUND = new Color(0, 0, 0, 128); + public static final Color BORDER = new Color(0, 0, 0, 128); + + public static ButtonContentPart textPart(Component component) { + return ButtonContentPart.newBuilder() + .setAdventureJsonText(AdventureUtil.toJson(component)) + .build(); + } + + public static ButtonContentPart iconPart(Icon icon) { + return ButtonContentPart.newBuilder() + .setIcon(icon) + .build(); + } + + private ChatButtonParts() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/StaffChatLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/StaffChatLayout.java new file mode 100644 index 00000000..66b7fb12 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/chatbuttons/StaffChatLayout.java @@ -0,0 +1,174 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.chatbuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.chat.v1.ChatButton; +import com.lunarclient.apollo.chat.v1.DisplayChatButtonsMessage; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class StaffChatLayout { + + public static void display(Player viewer) { + ChatButton staffChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("staff-chat") + .setPosition(HudPosition.newBuilder().setX(0).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(70).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("COMMAND_BLOCK", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Staff Chat", NamedTextColor.AQUA))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel staff") + .build()) + .build(); + + ChatButton publicChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("public-chat") + .setPosition(HudPosition.newBuilder().setX(76).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(78).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("OAK_SIGN", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Public Chat"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Click to switch!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/channel public") + .build()) + .build(); + + ChatButton clearChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("clear-chat") + .setPosition(HudPosition.newBuilder().setX(160).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(44).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("SPONGE", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Clear", NamedTextColor.YELLOW))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Clears the public chat", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/clearchat") + .build()) + .build(); + + ChatButton muteChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("mute-chat") + .setPosition(HudPosition.newBuilder().setX(210).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(44).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("BARRIER", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Mute", NamedTextColor.RED))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Mutes the public chat", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/mutechat") + .build()) + .build(); + + ChatButton unmuteChat = ChatButton.newBuilder() + .setButton(Button.newBuilder() + .setId("unmute-chat") + .setPosition(HudPosition.newBuilder().setX(260).setY(2).build()) + .setSize(ButtonSize.newBuilder().setWidth(56).setHeight(16).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(ChatButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(ChatButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(ChatButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("BELL", 0)) + .build())) + .addParts(ChatButtonParts.textPart(Component.text("Unmute", NamedTextColor.GREEN))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson( + Component.text("Unmutes the public chat", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/unmutechat") + .build()) + .build(); + + DisplayChatButtonsMessage message = DisplayChatButtonsMessage.newBuilder() + .addChatButtons(staffChat) + .addChatButtons(publicChat) + .addChatButtons(clearChat) + .addChatButtons(muteChat) + .addChatButtons(unmuteChat) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + private StaffChatLayout() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/HubLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/HubLayout.java new file mode 100644 index 00000000..8004ab4c --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/HubLayout.java @@ -0,0 +1,247 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.inventorybuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.common.v1.ItemStackIcon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.InventoryButton; +import com.lunarclient.apollo.inventory.v1.InventoryButtonBox; +import java.awt.Color; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class HubLayout { + + public static void display(Player viewer) { + InventoryButton practice = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("practice") + .setPosition(HudPosition.newBuilder().setX(6).setY(8).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ItemStackIcon.newBuilder() + .setItemName("SPLASH_POTION") + .setPotion("healing") + .build()) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Practice", NamedTextColor.LIGHT_PURPLE))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to join!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/server practice") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton factions = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("factions") + .setPosition(HudPosition.newBuilder().setX(6).setY(40).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("TNT", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Factions", NamedTextColor.RED))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to join!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/server factions") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton bedWars = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("bedwars") + .setPosition(HudPosition.newBuilder().setX(6).setY(72).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("RED_BED", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("BedWars", NamedTextColor.AQUA))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to join!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/server bedwars") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton soupPvP = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("souppvp") + .setPosition(HudPosition.newBuilder().setX(6).setY(104).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("MUSHROOM_STEW", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("SoupPvP", NamedTextColor.GOLD))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to join!", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/server souppvp") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton profile = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("profile") + .setPosition(HudPosition.newBuilder().setX(4).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + ProtobufUtil.createProfileProto( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text(viewer.getName(), NamedTextColor.GOLD))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("View your stats", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/profile") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton settings = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("settings") + .setPosition(HudPosition.newBuilder().setX(48).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(222, 160, 60, 85))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(255, 218, 150, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("COMPARATOR", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Settings", NamedTextColor.WHITE))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Server preferences", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/settings") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton changelog = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("changelog") + .setPosition(HudPosition.newBuilder().setX(6).setY(52).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("WRITABLE_BOOK", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Changelog"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("πŸŒ™ Apollo - v1.2.8", NamedTextColor.GOLD))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.empty())) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("β€’ Released Markers Module", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("β€’ Added ALLOW_DIG_AND_USE & DISABLE_BLOCK_MISS_PENALTY", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text(" options to Combat Module", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("β€’ Added configurable Server Link Button placement", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("β€’ Added API option to auto-enable Staff Mods", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text(" when unlocked via the Staff Mod Module", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("β€’ Improved performance with various optimizations", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.empty())) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Read the full changelog at", NamedTextColor.YELLOW))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8", NamedTextColor.YELLOW))) + .build()) + .setOpenUrl("https://github.com/LunarClient/Apollo/releases/tag/v1.2.8") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + DisplayInventoryButtonsMessage message = DisplayInventoryButtonsMessage.newBuilder() + .addInventoryButtons(practice) + .addInventoryButtons(factions) + .addInventoryButtons(bedWars) + .addInventoryButtons(soupPvP) + .addInventoryButtons(profile) + .addInventoryButtons(settings) + .addInventoryButtons(changelog) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + private HubLayout() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/InventoryButtonParts.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/InventoryButtonParts.java new file mode 100644 index 00000000..8160a437 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/InventoryButtonParts.java @@ -0,0 +1,70 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.inventorybuttons; + +import com.lunarclient.apollo.button.v1.ButtonContentPart; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.text.Component; + +public final class InventoryButtonParts { + + public static final Color BACKGROUND = new Color(255, 255, 255, 40); + public static final Color BORDER = new Color(37, 37, 37, 128); + + public static ButtonContentPart textPart(Component component) { + return ButtonContentPart.newBuilder() + .setAdventureJsonText(AdventureUtil.toJson(component)) + .build(); + } + + public static ButtonContentPart iconPart(Icon icon) { + return ButtonContentPart.newBuilder() + .setIcon(icon) + .build(); + } + + public static List tooltipJson(Component... lines) { + List json = new ArrayList<>(lines.length); + for (Component line : lines) { + json.add(AdventureUtil.toJson(line)); + } + + return json; + } + + public static ButtonTooltip tooltipMessage(List adventureJsonLines) { + return ButtonTooltip.newBuilder() + .addAllAdventureJsonLines(adventureJsonLines) + .build(); + } + + private InventoryButtonParts() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MenuLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MenuLayout.java new file mode 100644 index 00000000..9be7efc4 --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MenuLayout.java @@ -0,0 +1,338 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.inventorybuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.InventoryButton; +import com.lunarclient.apollo.inventory.v1.InventoryButtonBox; +import com.lunarclient.apollo.inventory.v1.InventoryType; +import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage; +import java.awt.Color; +import java.util.List; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +public final class MenuLayout { + + public static void display(Player viewer) { + InventoryButton shop = InventoryButton.newBuilder() + .setInventoryType(InventoryType.INVENTORY_TYPE_PLAYER) + .setButton(Button.newBuilder() + .setId("shop") + .setPosition(HudPosition.newBuilder().setX(4).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("EMERALD", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Shop", NamedTextColor.GREEN))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Browse categories and buy items", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to open", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/shop") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton spawn = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("spawn") + .setPosition(HudPosition.newBuilder().setX(48).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("RED_BED", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Spawn", NamedTextColor.AQUA))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Teleport back to spawn", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to teleport", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/spawn") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton warps = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("warps") + .setPosition(HudPosition.newBuilder().setX(4).setY(48).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("COMPASS", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Warps", NamedTextColor.AQUA))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Browse public warps", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to teleport", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/warps") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton enderChest = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("enderchest") + .setPosition(HudPosition.newBuilder().setX(48).setY(48).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_CHEST", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Open your personal storage", NamedTextColor.GRAY))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Click to open", NamedTextColor.YELLOW))) + .build()) + .setRunCommand("/enderchest") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton balance = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("balance") + .setPosition(HudPosition.newBuilder().setX(6).setY(92).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("GOLD_INGOT", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(balanceContent(viewer))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(balanceTooltip())) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton profile = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("profile") + .setPosition(HudPosition.newBuilder().setX(4).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + ProtobufUtil.createProfileProto( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text(viewer.getName(), NamedTextColor.GOLD))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("View your stats", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/profile") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton settings = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("settings") + .setPosition(HudPosition.newBuilder().setX(48).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(222, 160, 60, 85))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(255, 218, 150, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("COMPARATOR", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Settings", NamedTextColor.WHITE))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Server preferences", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/settings") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton vote = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("vote") + .setPosition(HudPosition.newBuilder().setX(6).setY(52).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(34, 204, 68, 64))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(190, 255, 205, 110))) + .setHoveredBackgroundColor(ProtobufUtil.createColorProto(new Color(34, 204, 68, 130))) + .setHoveredBorderColor(ProtobufUtil.createColorProto(new Color(190, 255, 205, 210))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Vote"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Vote", NamedTextColor.GREEN))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Vote daily for rewards", NamedTextColor.GRAY))) + .build()) + .setOpenUrl("https://example.com/vote") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton discord = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("discord") + .setPosition(HudPosition.newBuilder().setX(6).setY(84).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(88, 101, 242, 90))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(150, 160, 250, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Discord"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Discord", NamedTextColor.BLUE))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Join our community", NamedTextColor.GRAY))) + .build()) + .setOpenUrl("https://lunarclient.dev/discord") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton lobby = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("lobby") + .setPosition(HudPosition.newBuilder().setX(6).setY(136).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(224, 64, 64, 128))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(255, 200, 200, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Back to Lobby"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Back to Lobby", NamedTextColor.RED))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/lobby") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + DisplayInventoryButtonsMessage message = DisplayInventoryButtonsMessage.newBuilder() + .addInventoryButtons(shop) + .addInventoryButtons(spawn) + .addInventoryButtons(warps) + .addInventoryButtons(enderChest) + .addInventoryButtons(balance) + .addInventoryButtons(profile) + .addInventoryButtons(settings) + .addInventoryButtons(vote) + .addInventoryButtons(discord) + .addInventoryButtons(lobby) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + public static void sendBalanceUpdate(Player viewer) { + UpdateInventoryButtonMessage message = UpdateInventoryButtonMessage.newBuilder() + .setId("balance") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("GOLD_INGOT", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(balanceContent(viewer))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(balanceTooltip())) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + private static Component balanceContent(Player viewer) { + return Component.text("$" + String.format("%,d", getBalance(viewer.getUniqueId())), NamedTextColor.GOLD); + } + + private static List balanceTooltip() { + return InventoryButtonParts.tooltipJson(Component.text("Your balance", NamedTextColor.GOLD)); + } + + // Demo economy: replace with your economy plugin lookup (e.g. Vault) + private static long getBalance(UUID playerIdentifier) { + long drift = (System.currentTimeMillis() / 10_000L) % 250L; + return 1_000L + Math.abs(playerIdentifier.hashCode() % 4_000) + drift; + } + + private MenuLayout() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MinigameLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MinigameLayout.java new file mode 100644 index 00000000..a86f527d --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/MinigameLayout.java @@ -0,0 +1,251 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.inventorybuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonClientAction; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonTooltip; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.AdventureUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.InventoryButton; +import com.lunarclient.apollo.inventory.v1.InventoryButtonBox; +import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage; +import java.awt.Color; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; + +public final class MinigameLayout { + + public static void display(Player viewer) { + InventoryButton mapInfo = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("map-info") + .setPosition(HudPosition.newBuilder().setX(6).setY(8).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Map: Apollo", NamedTextColor.AQUA))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Built by the Lunar Client Team"))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Released in 2024", NamedTextColor.GRAY))) + .build()) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton kills = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("kills") + .setPosition(HudPosition.newBuilder().setX(6).setY(40).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("IRON_SWORD", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(killsContent(viewer))) + .setScale(1.0F) + .build()) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton lobby = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("lobby") + .setPosition(HudPosition.newBuilder().setX(6).setY(136).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(224, 64, 64, 128))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(255, 200, 200, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(Component.text("Back to Lobby"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Back to Lobby", NamedTextColor.RED))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Return to the main lobby", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/lobby") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton profile = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("profile") + .setPosition(HudPosition.newBuilder().setX(4).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto( + "PLAYER_HEAD", 0, null, // use "skull" for legacy with customModelData set to 3 + ProtobufUtil.createProfileProto( + UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"), + "e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19", + "" + ) + )) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text(viewer.getName(), NamedTextColor.GOLD))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("View your stats", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/profile") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton settings = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("settings") + .setPosition(HudPosition.newBuilder().setX(48).setY(4).build()) + .setSize(ButtonSize.newBuilder().setWidth(40).setHeight(40).build()) + .setShape(ButtonShape.BUTTON_SHAPE_CIRCLE) + .setBackgroundColor(ProtobufUtil.createColorProto(new Color(222, 160, 60, 85))) + .setBorderColor(ProtobufUtil.createColorProto(new Color(255, 218, 150, 140))) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("COMPARATOR", 0)) + .build())) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Settings", NamedTextColor.WHITE))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Server preferences", NamedTextColor.GRAY))) + .build()) + .setRunCommand("/settings") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton showMap = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("show-map") + .setPosition(HudPosition.newBuilder().setX(6).setY(52).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("FILLED_MAP", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Show Map"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Show Map", NamedTextColor.AQUA))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY))) + .build()) + .setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_MINIMAP_VIEW) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton waypoints = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("waypoints") + .setPosition(HudPosition.newBuilder().setX(6).setY(84).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("LODESTONE", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Waypoints"))) + .setScale(1.0F) + .build()) + .setTooltip(ButtonTooltip.newBuilder() + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Waypoints", NamedTextColor.GOLD))) + .addAdventureJsonLines(AdventureUtil.toJson(Component.text("Manage your waypoints", NamedTextColor.GRAY))) + .build()) + .setClientAction(ButtonClientAction.BUTTON_CLIENT_ACTION_OPEN_WAYPOINTS_MENU) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + DisplayInventoryButtonsMessage message = DisplayInventoryButtonsMessage.newBuilder() + .addInventoryButtons(mapInfo) + .addInventoryButtons(kills) + .addInventoryButtons(lobby) + .addInventoryButtons(profile) + .addInventoryButtons(settings) + .addInventoryButtons(showMap) + .addInventoryButtons(waypoints) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + public static void sendKillsUpdate(Player viewer) { + UpdateInventoryButtonMessage message = UpdateInventoryButtonMessage.newBuilder() + .setId("kills") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("IRON_SWORD", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(killsContent(viewer))) + .setScale(1.0F) + .build()) + .build()) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + private static Component killsContent(Player viewer) { + return Component.text("Kills: ", NamedTextColor.GRAY) + .append(Component.text(viewer.getStatistic(Statistic.PLAYER_KILLS), NamedTextColor.RED)); + } + + private MinigameLayout() { + } + +} diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/StaffLayout.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/StaffLayout.java new file mode 100644 index 00000000..01a8f2bb --- /dev/null +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/inventorybuttons/StaffLayout.java @@ -0,0 +1,398 @@ +/* + * This file is part of Apollo, licensed under the MIT License. + * + * Copyright (c) 2026 Moonsworth + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.lunarclient.apollo.example.proto.module.inventorybuttons; + +import com.lunarclient.apollo.button.v1.Button; +import com.lunarclient.apollo.button.v1.ButtonContent; +import com.lunarclient.apollo.button.v1.ButtonShape; +import com.lunarclient.apollo.button.v1.ButtonSize; +import com.lunarclient.apollo.button.v1.ButtonUpdate; +import com.lunarclient.apollo.common.v1.Icon; +import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; +import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.example.util.ServerStatsUtil; +import com.lunarclient.apollo.hud.v1.HudPosition; +import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage; +import com.lunarclient.apollo.inventory.v1.InventoryButton; +import com.lunarclient.apollo.inventory.v1.InventoryButtonBox; +import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public final class StaffLayout { + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); + + public static void display(Player viewer) { + InventoryButton players = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("players") + .setPosition(HudPosition.newBuilder().setX(6).setY(8).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(playersContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(playersTooltip())) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton tps = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("tps") + .setPosition(HudPosition.newBuilder().setX(6).setY(40).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(tpsContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(tpsTooltip())) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton cpu = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("cpu") + .setPosition(HudPosition.newBuilder().setX(6).setY(72).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(cpuContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(cpuTooltip())) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton ram = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("ram") + .setPosition(HudPosition.newBuilder().setX(6).setY(104).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(ramContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(ramTooltip())) + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_LEFT) + .build(); + + InventoryButton survival = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("survival") + .setPosition(HudPosition.newBuilder().setX(6).setY(8).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("IRON_SWORD", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Survival"))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(InventoryButtonParts.tooltipJson( + Component.text("Click to switch!", NamedTextColor.YELLOW)))) + .setRunCommand("/gamemode survival") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton creative = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("creative") + .setPosition(HudPosition.newBuilder().setX(6).setY(40).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("GRASS_BLOCK", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Creative"))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(InventoryButtonParts.tooltipJson( + Component.text("Click to switch!", NamedTextColor.YELLOW)))) + .setRunCommand("/gamemode creative") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton adventure = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("adventure") + .setPosition(HudPosition.newBuilder().setX(6).setY(72).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("FILLED_MAP", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Adventure"))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(InventoryButtonParts.tooltipJson( + Component.text("Click to switch!", NamedTextColor.YELLOW)))) + .setRunCommand("/gamemode adventure") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + InventoryButton spectator = InventoryButton.newBuilder() + .setButton(Button.newBuilder() + .setId("spectator") + .setPosition(HudPosition.newBuilder().setX(6).setY(104).build()) + .setSize(ButtonSize.newBuilder().setWidth(80).setHeight(26).build()) + .setShape(ButtonShape.BUTTON_SHAPE_ROUNDED_SQUARE) + .setBackgroundColor(ProtobufUtil.createColorProto(InventoryButtonParts.BACKGROUND)) + .setBorderColor(ProtobufUtil.createColorProto(InventoryButtonParts.BORDER)) + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.iconPart(Icon.newBuilder() + .setItemStack(ProtobufUtil.createItemStackIconProto("ENDER_EYE", 0)) + .build())) + .addParts(InventoryButtonParts.textPart(Component.text("Spectator"))) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(InventoryButtonParts.tooltipJson( + Component.text("Click to switch!", NamedTextColor.YELLOW)))) + .setRunCommand("/gamemode spectator") + .build()) + .setBox(InventoryButtonBox.INVENTORY_BUTTON_BOX_RIGHT) + .build(); + + DisplayInventoryButtonsMessage message = DisplayInventoryButtonsMessage.newBuilder() + .addInventoryButtons(players) + .addInventoryButtons(tps) + .addInventoryButtons(cpu) + .addInventoryButtons(ram) + .addInventoryButtons(survival) + .addInventoryButtons(creative) + .addInventoryButtons(adventure) + .addInventoryButtons(spectator) + .build(); + + ProtobufPacketUtil.sendPacket(viewer, message); + } + + public static void sendUpdates(Player viewer) { + ProtobufPacketUtil.sendPacket(viewer, UpdateInventoryButtonMessage.newBuilder() + .setId("players") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(playersContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(playersTooltip())) + .build()) + .build()); + + ProtobufPacketUtil.sendPacket(viewer, UpdateInventoryButtonMessage.newBuilder() + .setId("tps") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(tpsContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(tpsTooltip())) + .build()) + .build()); + + ProtobufPacketUtil.sendPacket(viewer, UpdateInventoryButtonMessage.newBuilder() + .setId("cpu") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(cpuContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(cpuTooltip())) + .build()) + .build()); + + ProtobufPacketUtil.sendPacket(viewer, UpdateInventoryButtonMessage.newBuilder() + .setId("ram") + .setUpdate(ButtonUpdate.newBuilder() + .setContent(ButtonContent.newBuilder() + .addParts(InventoryButtonParts.textPart(ramContent())) + .setScale(1.0F) + .build()) + .setTooltip(InventoryButtonParts.tooltipMessage(ramTooltip())) + .build()) + .build()); + } + + private static Component playersContent() { + return Component.text("Players: ", NamedTextColor.GRAY) + .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)); + } + + private static List playersTooltip() { + return InventoryButtonParts.tooltipJson( + Component.text("Players currently online", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + private static Component tpsContent() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length == 0) { + return Component.text("TPS: N/A", NamedTextColor.GRAY); + } + + double recent = Math.min(20.0D, tps[0]); + return Component.text("TPS: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.1f", recent), tpsColor(recent))); + } + + private static List tpsTooltip() { + double[] tps = ServerStatsUtil.getTps(); + if (tps == null || tps.length < 3) { + return InventoryButtonParts.tooltipJson( + Component.text("TPS averages require a Paper based server", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + return InventoryButtonParts.tooltipJson( + tpsAverageLine("1m", tps[0]), + tpsAverageLine("5m", tps[1]), + tpsAverageLine("15m", tps[2]), + Component.empty(), + refreshedLine()); + } + + private static Component tpsAverageLine(String window, double average) { + double tps = Math.min(20.0D, average); + return Component.text(window + ": ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", tps), tpsColor(tps))); + } + + private static NamedTextColor tpsColor(double tps) { + if (tps >= 18.0D) { + return NamedTextColor.GREEN; + } + + return tps >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component cpuContent() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return Component.text("CPU: N/A", NamedTextColor.GRAY); + } + + return Component.text("CPU: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.2f", load), cpuColor(load))); + } + + private static List cpuTooltip() { + double load = ServerStatsUtil.getSystemLoadAverage(); + if (load < 0.0D) { + return InventoryButtonParts.tooltipJson( + Component.text("The system load average is unavailable", NamedTextColor.GRAY), + Component.empty(), + refreshedLine()); + } + + int cores = ServerStatsUtil.getAvailableProcessors(); + double perCore = load * 100.0D / cores; + return InventoryButtonParts.tooltipJson( + Component.text("System load average (last minute)", NamedTextColor.GRAY), + Component.text("Cores: ", NamedTextColor.GRAY) + .append(Component.text(cores, NamedTextColor.WHITE)), + Component.text("Per core: ", NamedTextColor.GRAY) + .append(Component.text(String.format("%.0f%%", perCore), cpuColor(load))), + Component.empty(), + refreshedLine()); + } + + private static NamedTextColor cpuColor(double load) { + double perCore = load / ServerStatsUtil.getAvailableProcessors(); + if (perCore < 0.5D) { + return NamedTextColor.GREEN; + } + + return perCore < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component ramContent() { + long used = ServerStatsUtil.getUsedRamMb(); + long max = ServerStatsUtil.getMaxRamMb(); + long percent = max <= 0 ? 0 : used * 100 / max; + + return Component.text("RAM: ", NamedTextColor.GRAY) + .append(Component.text(percent + "%", ramColor(percent))); + } + + private static List ramTooltip() { + return InventoryButtonParts.tooltipJson( + Component.text("Used: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getUsedRamMb() + " MB", NamedTextColor.WHITE)), + Component.text("Max: ", NamedTextColor.GRAY) + .append(Component.text(ServerStatsUtil.getMaxRamMb() + " MB", NamedTextColor.WHITE)), + Component.empty(), + refreshedLine()); + } + + private static NamedTextColor ramColor(long percent) { + if (percent < 60) { + return NamedTextColor.GREEN; + } + + return percent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED; + } + + private static Component refreshedLine() { + return Component.text("Updated at " + LocalTime.now().format(TIME_FORMAT), NamedTextColor.YELLOW, TextDecoration.ITALIC); + } + + private StaffLayout() { + } + +} 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 f8a0e17c..b5171f58 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 @@ -59,6 +59,8 @@ public final class ProtobufPacketUtil { 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()); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-chat-close.send-packet", Value.newBuilder().setBoolValue(false).build()); + CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-inventory-open.send-packet", Value.newBuilder().setBoolValue(false).build()); + CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-inventory-close.send-packet", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-use-item.send-packet", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("packet_enrichment", "player-use-item-bucket.send-packet", Value.newBuilder().setBoolValue(false).build()); CONFIG_MODULE_PROPERTIES.put("server_link", "legacy-button-placement", Value.newBuilder().setStringValue("NEW_ROW").build()); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f52700a..87772d71 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ geantyref = "1.3.11" idea = "1.1.7" jetbrains = "24.0.1" lombok = "1.18.38" -protobuf = "0.2.1" +protobuf = "0.2.2" gson = "2.10.1" shadow = "9.4.1" spotless = "8.4.0" diff --git a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java index c485822d..dd49d311 100644 --- a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java +++ b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java @@ -55,6 +55,7 @@ import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; import com.lunarclient.apollo.module.limb.LimbModuleImpl; import com.lunarclient.apollo.module.marker.MarkerModule; @@ -100,6 +101,7 @@ import com.lunarclient.apollo.stats.ApolloStats; import com.lunarclient.apollo.wrapper.BukkitApolloStats; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import lombok.Getter; @@ -153,7 +155,7 @@ public void onEnable() { .addModule(GlowModule.class, new GlowModuleImpl()) .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) - .addModule(InventoryModule.class) + .addModule(InventoryModule.class, new InventoryModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) .addModule(ModSettingModule.class, new ModSettingModuleImpl()) @@ -227,6 +229,19 @@ public ApolloStats getStats() { return this.stats; } + private final Scheduler scheduler = new Scheduler() { + @Override + public void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit) { + Bukkit.getScheduler().runTaskTimerAsynchronously(ApolloBukkitPlatform.this.plugin, task, + Math.max(1L, unit.toMillis(delay) / 50L), Math.max(1L, unit.toMillis(period) / 50L)); + } + }; + + @Override + public Scheduler getScheduler() { + return this.scheduler; + } + @Override public Logger getPlatformLogger() { return Bukkit.getServer().getLogger(); diff --git a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java index 74215459..2c35eb66 100644 --- a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java +++ b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java @@ -50,6 +50,8 @@ import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; import com.lunarclient.apollo.module.limb.LimbModuleImpl; import com.lunarclient.apollo.module.marker.MarkerModule; @@ -88,6 +90,7 @@ import com.lunarclient.apollo.stats.ApolloStats; import com.lunarclient.apollo.wrapper.BungeeApolloStats; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import lombok.Getter; @@ -135,6 +138,7 @@ public void onEnable() { .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) + .addModule(InventoryModule.class, new InventoryModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) .addModule(ModSettingModule.class, new ModSettingModuleImpl()) @@ -205,4 +209,17 @@ public ApolloStats getStats() { return this.stats; } + private final Scheduler scheduler = new Scheduler() { + @Override + public void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit) { + ApolloBungeePlatform.this.plugin.getProxy().getScheduler() + .schedule(ApolloBungeePlatform.this.plugin, task, delay, period, unit); + } + }; + + @Override + public Scheduler getScheduler() { + return this.scheduler; + } + } diff --git a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java index 5255e8e3..1bf3caea 100644 --- a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java +++ b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java @@ -52,6 +52,8 @@ import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; import com.lunarclient.apollo.module.limb.LimbModuleImpl; import com.lunarclient.apollo.module.marker.MarkerModule; @@ -96,6 +98,7 @@ import com.lunarclient.apollo.stats.ApolloStats; import com.lunarclient.apollo.wrapper.FoliaApolloStats; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import lombok.Getter; @@ -142,6 +145,7 @@ public void onEnable() { .addModule(GlowModule.class, new GlowModuleImpl()) .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) + .addModule(InventoryModule.class, new InventoryModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) .addModule(ModSettingModule.class, new ModSettingModuleImpl()) @@ -210,6 +214,19 @@ public ApolloStats getStats() { return this.stats; } + private final Scheduler scheduler = new Scheduler() { + @Override + public void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit) { + Bukkit.getAsyncScheduler().runAtFixedRate(ApolloFoliaPlatform.this, + scheduledTask -> task.run(), delay, period, unit); + } + }; + + @Override + public Scheduler getScheduler() { + return this.scheduler; + } + @Override public Object getPlugin() { return getInstance(); diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java index b1d97ad7..c0d1d3cf 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -54,6 +54,7 @@ import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; import com.lunarclient.apollo.module.limb.LimbModuleImpl; import com.lunarclient.apollo.module.marker.MarkerModule; @@ -98,9 +99,12 @@ import com.lunarclient.apollo.option.OptionsImpl; import com.lunarclient.apollo.stats.ApolloStats; import com.lunarclient.apollo.wrapper.MinestomApolloStats; +import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import lombok.Getter; @@ -175,7 +179,7 @@ public static void init(ApolloMinestomProperties properties) { .addModule(GlowModule.class, new GlowModuleImpl()) .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) - .addModule(InventoryModule.class) + .addModule(InventoryModule.class, new InventoryModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) .addModule(ModSettingModule.class, new ModSettingModuleImpl()) @@ -257,6 +261,22 @@ public ApolloStats getStats() { return this.stats; } + private final Scheduler scheduler = new Scheduler() { + @Override + public void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit) { + MinecraftServer.getSchedulerManager() + .buildTask(() -> ForkJoinPool.commonPool().execute(task)) + .delay(Duration.ofMillis(unit.toMillis(delay))) + .repeat(Duration.ofMillis(unit.toMillis(period))) + .schedule(); + } + }; + + @Override + public Scheduler getScheduler() { + return this.scheduler; + } + @Override public Logger getPlatformLogger() { return this.logger; 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 3b801511..6c9138c1 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -50,6 +50,8 @@ import com.lunarclient.apollo.module.heightlimit.HeightLimitModuleImpl; import com.lunarclient.apollo.module.hologram.HologramModule; import com.lunarclient.apollo.module.hologram.HologramModuleImpl; +import com.lunarclient.apollo.module.inventory.InventoryModule; +import com.lunarclient.apollo.module.inventory.InventoryModuleImpl; import com.lunarclient.apollo.module.limb.LimbModule; import com.lunarclient.apollo.module.limb.LimbModuleImpl; import com.lunarclient.apollo.module.marker.MarkerModule; @@ -102,6 +104,7 @@ import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import java.nio.file.Path; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import lombok.Getter; @@ -171,6 +174,22 @@ public ApolloStats getStats() { return this.stats; } + private final Scheduler scheduler = new Scheduler() { + @Override + public void scheduleAsyncRepeating(Runnable task, long delay, long period, TimeUnit unit) { + ApolloVelocityPlatform.this.server.getScheduler() + .buildTask(ApolloVelocityPlatform.this, task) + .delay(delay, unit) + .repeat(period, unit) + .schedule(); + } + }; + + @Override + public Scheduler getScheduler() { + return this.scheduler; + } + @Override public Object getPlugin() { return getInstance(); @@ -202,6 +221,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(HeightLimitModule.class, new HeightLimitModuleImpl()) .addModule(HologramModule.class, new HologramModuleImpl()) + .addModule(InventoryModule.class, new InventoryModuleImpl()) .addModule(LimbModule.class, new LimbModuleImpl()) .addModule(MarkerModule.class, new MarkerModuleImpl()) .addModule(ModSettingModule.class, new ModSettingModuleImpl()) From 60a22d05003126cd1e85789fe79e18cae14625a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 17 Aug 2026 14:47:07 +0200 Subject: [PATCH 7/9] Add `NametagVisibilityOverride` to `Nametag` (#305) # Conflicts: # gradle/libs.versions.toml --- .../apollo/module/nametag/Nametag.java | 9 +++ .../nametag/NametagVisibilityOverride.java | 59 +++++++++++++++++++ .../module/nametag/NametagModuleImpl.java | 9 +++ docs/developers/lightweight/protobuf.mdx | 6 +- docs/developers/modules/nametag.mdx | 12 ++++ .../example/api/module/NametagApiExample.java | 2 + .../json/module/NametagJsonExample.java | 1 + .../proto/module/NametagProtoExample.java | 2 + gradle/libs.versions.toml | 2 +- 9 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/module/nametag/NametagVisibilityOverride.java diff --git a/api/src/main/java/com/lunarclient/apollo/module/nametag/Nametag.java b/api/src/main/java/com/lunarclient/apollo/module/nametag/Nametag.java index 5089ffd8..a45544b3 100644 --- a/api/src/main/java/com/lunarclient/apollo/module/nametag/Nametag.java +++ b/api/src/main/java/com/lunarclient/apollo/module/nametag/Nametag.java @@ -45,4 +45,13 @@ public final class Nametag { */ List lines; + /** + * Returns the {@link NametagVisibilityOverride} override for this nametag. + * + * @return the visibility override + * @since 1.2.9 + */ + @Builder.Default + NametagVisibilityOverride visibilityOverride = NametagVisibilityOverride.NONE; + } diff --git a/api/src/main/java/com/lunarclient/apollo/module/nametag/NametagVisibilityOverride.java b/api/src/main/java/com/lunarclient/apollo/module/nametag/NametagVisibilityOverride.java new file mode 100644 index 00000000..4c0b5ff2 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/module/nametag/NametagVisibilityOverride.java @@ -0,0 +1,59 @@ +/* + * 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.nametag; + +/** + * Represents an override of the vanilla team-based nametag visibility + * outcome for the target player, from the viewpoint of the receiving player. + * + * @since 1.2.9 + */ +public enum NametagVisibilityOverride { + + /** + * The vanilla team-based nametag visibility applies. + * + * @since 1.2.9 + */ + NONE, + + /** + * Shows the nametag, as if the target's team nametag visibility + * were {@code always}, still respecting invisibility. + * + * @since 1.2.9 + */ + SHOWN, + + /** + * Hides the nametag, as if the target's team nametag visibility + * were {@code never}. + * + *

Applies even when the target is not on any team.

+ * + * @since 1.2.9 + */ + HIDDEN + +} diff --git a/common/src/main/java/com/lunarclient/apollo/module/nametag/NametagModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/nametag/NametagModuleImpl.java index 5254595d..e055f0a0 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/nametag/NametagModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/nametag/NametagModuleImpl.java @@ -50,6 +50,11 @@ public void overrideNametag(@NonNull Recipients recipients, @NonNull UUID player builder.addAdventureJsonLines(ApolloComponent.toJson(line)); } + NametagVisibilityOverride visibility = nametag.getVisibilityOverride(); + if (visibility != null && visibility != NametagVisibilityOverride.NONE) { + builder.setVisibilityOverride(this.toProtobuf(visibility)); + } + ApolloManager.getNetworkManager().sendPacket(recipients, builder.build()); } @@ -68,4 +73,8 @@ public void resetNametags(@NonNull Recipients recipients) { ApolloManager.getNetworkManager().sendPacket(recipients, message); } + private com.lunarclient.apollo.nametag.v1.NametagVisibilityOverride toProtobuf(NametagVisibilityOverride visibility) { + return com.lunarclient.apollo.nametag.v1.NametagVisibilityOverride.forNumber(visibility.ordinal() + 1); + } + } diff --git a/docs/developers/lightweight/protobuf.mdx b/docs/developers/lightweight/protobuf.mdx index 2759440a..0bb373d6 100644 --- a/docs/developers/lightweight/protobuf.mdx +++ b/docs/developers/lightweight/protobuf.mdx @@ -26,7 +26,7 @@ Available fields for each message, including their types, are available on the B com.lunarclient apollo-protos - 0.2.1 + 0.2.4 ``` @@ -41,7 +41,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api 'com.lunarclient:apollo-protos:0.2.1' + api 'com.lunarclient:apollo-protos:0.2.4' } ``` @@ -55,7 +55,7 @@ Available fields for each message, including their types, are available on the B } dependencies { - api("com.lunarclient:apollo-protos:0.2.1") + api("com.lunarclient:apollo-protos:0.2.4") } ``` diff --git a/docs/developers/modules/nametag.mdx b/docs/developers/modules/nametag.mdx index 081f8acd..0c8ecd3d 100644 --- a/docs/developers/modules/nametag.mdx +++ b/docs/developers/modules/nametag.mdx @@ -50,6 +50,7 @@ public void overrideNametagExample(Player target) { .color(NamedTextColor.RED) .build() )) + .visibilityOverride(NametagVisibilityOverride.NONE) .build() ); } @@ -100,6 +101,15 @@ public void resetNametagsExample(Player viewer) { )) ``` +`.visibilityOverride(NametagVisibilityOverride)` overrides the vanilla team-based nametag visibility outcome for the target player, from the viewpoint of the receiving player. +- `SHOWN` displays the nametag as if the target's team nametag visibility were `always` (still respecting invisibility) +- `HIDDEN` hides the nametag as if it were `never`, even when the target is not on any team. +- `NONE` keeps the vanilla behavior. + +```java +.visibilityOverride(NametagVisibilityOverride.NONE) +``` + @@ -129,6 +139,7 @@ public void overrideNametagExample(Player target) { OverrideNametagMessage message = OverrideNametagMessage.newBuilder() .setPlayerUuid(ProtobufUtil.createUuidProto(target.getUniqueId())) .addAllAdventureJsonLines(lines) + .setVisibilityOverride(NametagVisibilityOverride.NAMETAG_VISIBILITY_OVERRIDE_NONE) .build(); ProtobufPacketUtil.broadcastPacket(message); @@ -186,6 +197,7 @@ public void overrideNametagExample(Player target) { message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.nametag.v1.OverrideNametagMessage"); message.add("player_uuid", JsonUtil.createUuidObject(target.getUniqueId())); message.add("adventure_json_lines", lines); + message.addProperty("visibility_override", "NAMETAG_VISIBILITY_OVERRIDE_NONE"); JsonPacketUtil.broadcastPacket(message); } diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/NametagApiExample.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/NametagApiExample.java index 132d5d86..a6af0c60 100644 --- a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/NametagApiExample.java +++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/NametagApiExample.java @@ -28,6 +28,7 @@ import com.lunarclient.apollo.example.module.impl.NametagExample; import com.lunarclient.apollo.module.nametag.Nametag; import com.lunarclient.apollo.module.nametag.NametagModule; +import com.lunarclient.apollo.module.nametag.NametagVisibilityOverride; import com.lunarclient.apollo.player.ApolloPlayer; import com.lunarclient.apollo.recipients.Recipients; import java.util.Optional; @@ -54,6 +55,7 @@ public void overrideNametagExample(Player target) { .color(NamedTextColor.RED) .build() )) + .visibilityOverride(NametagVisibilityOverride.NONE) .build() ); } diff --git a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/NametagJsonExample.java b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/NametagJsonExample.java index 0b2f1de7..460b4cd5 100644 --- a/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/NametagJsonExample.java +++ b/example/bukkit/json/src/main/java/com/lunarclient/apollo/example/json/module/NametagJsonExample.java @@ -58,6 +58,7 @@ public void overrideNametagExample(Player target) { message.addProperty("@type", "type.googleapis.com/lunarclient.apollo.nametag.v1.OverrideNametagMessage"); message.add("player_uuid", JsonUtil.createUuidObject(target.getUniqueId())); message.add("adventure_json_lines", lines); + message.addProperty("visibility_override", "NAMETAG_VISIBILITY_OVERRIDE_NONE"); JsonPacketUtil.broadcastPacket(message); } diff --git a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/NametagProtoExample.java b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/NametagProtoExample.java index 07e5dbeb..8c59b086 100644 --- a/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/NametagProtoExample.java +++ b/example/bukkit/proto/src/main/java/com/lunarclient/apollo/example/proto/module/NametagProtoExample.java @@ -28,6 +28,7 @@ import com.lunarclient.apollo.example.proto.util.AdventureUtil; import com.lunarclient.apollo.example.proto.util.ProtobufPacketUtil; import com.lunarclient.apollo.example.proto.util.ProtobufUtil; +import com.lunarclient.apollo.nametag.v1.NametagVisibilityOverride; import com.lunarclient.apollo.nametag.v1.OverrideNametagMessage; import com.lunarclient.apollo.nametag.v1.ResetNametagMessage; import com.lunarclient.apollo.nametag.v1.ResetNametagsMessage; @@ -59,6 +60,7 @@ public void overrideNametagExample(Player target) { OverrideNametagMessage message = OverrideNametagMessage.newBuilder() .setPlayerUuid(ProtobufUtil.createUuidProto(target.getUniqueId())) .addAllAdventureJsonLines(lines) + .setVisibilityOverride(NametagVisibilityOverride.NAMETAG_VISIBILITY_OVERRIDE_NONE) .build(); ProtobufPacketUtil.broadcastPacket(message); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 87772d71..5099e64b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ geantyref = "1.3.11" idea = "1.1.7" jetbrains = "24.0.1" lombok = "1.18.38" -protobuf = "0.2.2" +protobuf = "0.2.4" gson = "2.10.1" shadow = "9.4.1" spotless = "8.4.0" From c992ee7eaebd0ac83b12ffb7fe15758fbaf1cac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 17 Aug 2026 14:49:55 +0200 Subject: [PATCH 8/9] Sync LunarClient Mods & Options (#307) * Sync LunarClient Mods & Options * Update version tags to 1.2.9 --------- Co-authored-by: LunarClient Bot --- .../com/lunarclient/apollo/mods/Mods.java | 12 +- .../apollo/mods/impl/ModActionBar.java | 63 ++ .../lunarclient/apollo/mods/impl/ModChat.java | 60 ++ .../apollo/mods/impl/ModHitbox.java | 718 ++++++++++++++++++ .../apollo/mods/impl/ModKnockbackTrainer.java | 98 +++ .../apollo/mods/impl/ModLightOverlay.java | 208 +++++ .../apollo/mods/impl/ModNametag.java | 12 + .../apollo/mods/impl/ModOverlayMod.java | 12 + .../lunarclient/apollo/mods/impl/ModPing.java | 6 +- .../apollo/mods/impl/ModPotionCounter.java | 213 ++++++ .../apollo/mods/impl/ModPotionEffects.java | 280 +++++-- .../apollo/mods/impl/ModScoreboard.java | 11 + .../apollo/mods/impl/ModScreenshot.java | 11 + .../apollo/mods/impl/ModSkyblock.java | 22 +- .../apollo/mods/impl/ModTierTagger.java | 57 +- docs/developers/mods/_meta.json | 4 + docs/developers/mods/actionbar.mdx | 31 + docs/developers/mods/chat.mdx | 35 + docs/developers/mods/hitbox.mdx | 393 ++++++++++ docs/developers/mods/knockbacktrainer.mdx | 53 ++ docs/developers/mods/lightoverlay.mdx | 121 +++ docs/developers/mods/nametag.mdx | 7 + docs/developers/mods/overlaymod.mdx | 8 + docs/developers/mods/ping.mdx | 4 +- docs/developers/mods/potioncounter.mdx | 118 +++ docs/developers/mods/potioneffects.mdx | 177 +++-- docs/developers/mods/scoreboard.mdx | 6 + docs/developers/mods/screenshot.mdx | 6 + docs/developers/mods/skyblock.mdx | 14 +- docs/developers/mods/tiertagger.mdx | 32 +- 30 files changed, 2668 insertions(+), 124 deletions(-) create mode 100644 api/src/main/java/com/lunarclient/apollo/mods/impl/ModActionBar.java create mode 100644 api/src/main/java/com/lunarclient/apollo/mods/impl/ModKnockbackTrainer.java create mode 100644 api/src/main/java/com/lunarclient/apollo/mods/impl/ModLightOverlay.java create mode 100644 api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionCounter.java create mode 100644 docs/developers/mods/actionbar.mdx create mode 100644 docs/developers/mods/knockbacktrainer.mdx create mode 100644 docs/developers/mods/lightoverlay.mdx create mode 100644 docs/developers/mods/potioncounter.mdx diff --git a/api/src/main/java/com/lunarclient/apollo/mods/Mods.java b/api/src/main/java/com/lunarclient/apollo/mods/Mods.java index 344016d6..98e66355 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/Mods.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/Mods.java @@ -25,6 +25,7 @@ import com.lunarclient.apollo.mods.impl.Mod2dItems; import com.lunarclient.apollo.mods.impl.Mod3dSkins; +import com.lunarclient.apollo.mods.impl.ModActionBar; import com.lunarclient.apollo.mods.impl.ModArmorstatus; import com.lunarclient.apollo.mods.impl.ModAttackIndicator; import com.lunarclient.apollo.mods.impl.ModAudioSubtitles; @@ -64,6 +65,8 @@ import com.lunarclient.apollo.mods.impl.ModItemTracker; import com.lunarclient.apollo.mods.impl.ModKeystrokes; import com.lunarclient.apollo.mods.impl.ModKillSounds; +import com.lunarclient.apollo.mods.impl.ModKnockbackTrainer; +import com.lunarclient.apollo.mods.impl.ModLightOverlay; import com.lunarclient.apollo.mods.impl.ModLighting; import com.lunarclient.apollo.mods.impl.ModMarkers; import com.lunarclient.apollo.mods.impl.ModMemory; @@ -83,6 +86,7 @@ import com.lunarclient.apollo.mods.impl.ModParticleChanger; import com.lunarclient.apollo.mods.impl.ModPing; import com.lunarclient.apollo.mods.impl.ModPlaytime; +import com.lunarclient.apollo.mods.impl.ModPotionCounter; import com.lunarclient.apollo.mods.impl.ModPotionEffects; import com.lunarclient.apollo.mods.impl.ModPvpInfo; import com.lunarclient.apollo.mods.impl.ModQuickplay; @@ -155,6 +159,7 @@ public final class Mods { ModScoreboard.class, ModTitles.class, ModItemCounter.class, + ModPotionCounter.class, ModPing.class, ModMotionBlur.class, ModPackOrganizer.class, @@ -218,15 +223,18 @@ public final class Mods { ModOverlayMod.class, ModRewind.class, ModAudioSubtitles.class, + ModActionBar.class, ModShields.class, + ModLightOverlay.class, ModKillSounds.class, ModInventoryMod.class, ModF3Display.class, ModGuiScale.class, + ModKnockbackTrainer.class, ModRadio.class, - ModSba.class, ModUhcOverlay.class, - ModNeu.class + ModNeu.class, + ModSba.class ); private Mods() { diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModActionBar.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModActionBar.java new file mode 100644 index 00000000..d416060f --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModActionBar.java @@ -0,0 +1,63 @@ +/* + * 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.mods.impl; + +import com.lunarclient.apollo.option.NumberOption; +import com.lunarclient.apollo.option.SimpleOption; +import io.leangen.geantyref.TypeToken; + +/** + * Allows you to move and scale Minecraft's action bar. + * + * @since 1.2.9 + */ +public final class ModActionBar { + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption ENABLED = SimpleOption.builder() + .node("action-bar", "enabled").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption SCALE = NumberOption.number() + .node("action-bar", "scale").type(TypeToken.get(Float.class)) + .min(0.25F).max(5.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + + private ModActionBar() { + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModChat.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModChat.java index 231f2f69..b428e8c2 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModChat.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModChat.java @@ -207,6 +207,66 @@ public final class ModChat { .notifyClient() .build(); + /** + * Displays the player's Minecraft head before their message in chat. + * + * @since 1.2.9 + */ + public static final SimpleOption CHAT_HEADS = SimpleOption.builder() + .comment("Displays the player's Minecraft head before their message in chat.") + .node("chat", "chat-heads").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Looks up skins for chat senders that aren't in the tab list, like players on other servers of a network. Head detection may be less accurate. + * + * @since 1.2.9 + */ + public static final SimpleOption CHAT_HEADS_FETCH_UNKNOWN = SimpleOption.builder() + .comment("Looks up skins for chat senders that aren't in the tab list, like players on other servers of a network. Head detection may be less accurate.") + .node("chat", "chat-heads-fetch-unknown").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Adds emojis to chat. Shortcodes like :smile: autocomplete as you type, and emojis in messages show as colorful Twemoji. + * + * @since 1.2.9 + */ + public static final SimpleOption CHAT_EMOJI = SimpleOption.builder() + .comment("Adds emojis to chat. Shortcodes like :smile: autocomplete as you type, and emojis in messages show as colorful Twemoji.") + .node("chat", "chat-emoji").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * Shows emojis with Minecraft's built-in font instead of Twemoji images. Shortcodes the game font can't display are left as text. + * + * @since 1.2.9 + */ + public static final SimpleOption CHAT_EMOJI_UNICODE_ONLY = SimpleOption.builder() + .comment("Shows emojis with Minecraft's built-in font instead of Twemoji images. Shortcodes the game font can't display are left as text.") + .node("chat", "chat-emoji-unicode-only").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Turns shortcodes like :skull: in other players' messages into emojis. When disabled, only emojis sent as actual characters are shown. + * + * @since 1.2.9 + */ + public static final SimpleOption CHAT_EMOJI_CONVERT_SHORTCODES = SimpleOption.builder() + .comment("Turns shortcodes like :skull: in other players' messages into emojis. When disabled, only emojis sent as actual characters are shown.") + .node("chat", "chat-emoji-convert-shortcodes").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + /** * Copies the hovered chat message when holding the keybind and clicking. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModHitbox.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModHitbox.java index 715abbd2..66109a77 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModHitbox.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModHitbox.java @@ -104,6 +104,62 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PLAYER_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-player-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PLAYER_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-player-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * Only show the hitbox when the entity is within reach and under your crosshair. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PLAYER_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .comment("Only show the hitbox when the entity is within reach and under your crosshair") + .node("hitbox", "hitbox-player-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PLAYER_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-player-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PLAYER_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-player-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -149,6 +205,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-item-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -194,6 +305,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_EXP_ORB_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-exp-orb-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_EXP_ORB_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-exp-orb-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_EXP_ORB_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-exp-orb-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_EXP_ORB_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-exp-orb-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_EXP_ORB_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-exp-orb-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -239,6 +405,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_FRAME_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-frame-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_FRAME_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-frame-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_FRAME_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-item-frame-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_FRAME_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-frame-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ITEM_FRAME_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-item-frame-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -284,6 +505,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREWORK_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-firework-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREWORK_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-firework-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREWORK_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-firework-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREWORK_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-firework-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREWORK_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-firework-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -329,6 +605,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_WITHER_SKULL_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-wither-skull-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_WITHER_SKULL_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-wither-skull-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_WITHER_SKULL_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-wither-skull-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_WITHER_SKULL_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-wither-skull-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_WITHER_SKULL_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-wither-skull-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -374,6 +705,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_SNOWBALL_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-snowball-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_SNOWBALL_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-snowball-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_SNOWBALL_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-snowball-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_SNOWBALL_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-snowball-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_SNOWBALL_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-snowball-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -419,6 +805,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREBALL_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-fireball-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREBALL_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-fireball-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREBALL_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-fireball-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREBALL_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-fireball-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_FIREBALL_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-fireball-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -464,6 +905,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ARROW_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-arrow-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ARROW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-arrow-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ARROW_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-arrow-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ARROW_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-arrow-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_ARROW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-arrow-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -509,6 +1005,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PROJECTILE_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-projectile-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PROJECTILE_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-projectile-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PROJECTILE_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-projectile-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PROJECTILE_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-projectile-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PROJECTILE_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-projectile-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -554,6 +1105,62 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_MONSTER_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-monster-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_MONSTER_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-monster-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * Only show the hitbox when the entity is within reach and under your crosshair. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_MONSTER_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .comment("Only show the hitbox when the entity is within reach and under your crosshair") + .node("hitbox", "hitbox-monster-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_MONSTER_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-monster-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_MONSTER_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-monster-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -599,6 +1206,62 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PASSIVE_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-passive-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PASSIVE_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-passive-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * Only show the hitbox when the entity is within reach and under your crosshair. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PASSIVE_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .comment("Only show the hitbox when the entity is within reach and under your crosshair") + .node("hitbox", "hitbox-passive-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PASSIVE_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-passive-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_PASSIVE_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-passive-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -644,6 +1307,61 @@ public final class ModHitbox { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_OTHER_SHOW_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-other-show-hittable-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_OTHER_HITTABLE_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-other-hittable-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_OTHER_ONLY_SHOW_HITTABLE = SimpleOption.builder() + .node("hitbox", "hitbox-other-only-show-hittable").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_OTHER_SHOW_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-other-show-damaged-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption HITBOX_OTHER_DAMAGED_COLOR = SimpleOption.builder() + .node("hitbox", "hitbox-other-damaged-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + /** * No documentation available. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModKnockbackTrainer.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModKnockbackTrainer.java new file mode 100644 index 00000000..1a82c014 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModKnockbackTrainer.java @@ -0,0 +1,98 @@ +/* + * 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.mods.impl; + +import com.lunarclient.apollo.option.NumberOption; +import com.lunarclient.apollo.option.SimpleOption; +import io.leangen.geantyref.TypeToken; + +/** + * Train jump resets: how close your jump press lands to the tick you were hit. + * + * @since 1.2.9 + */ +public final class ModKnockbackTrainer { + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption ENABLED = SimpleOption.builder() + .node("knockback-trainer", "enabled").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption TARGET_TICKS = NumberOption.number() + .node("knockback-trainer", "target-ticks").type(TypeToken.get(Integer.class)) + .min(0).max(6) + .defaultValue(0) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption WINDOW_TICKS = NumberOption.number() + .node("knockback-trainer", "window-ticks").type(TypeToken.get(Integer.class)) + .min(0).max(10) + .defaultValue(5) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption FALLING_HIT_SOUND = SimpleOption.builder() + .node("knockback-trainer", "falling-hit-sound").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption FALLING_HIT_SOUND_VOLUME = NumberOption.number() + .node("knockback-trainer", "falling-hit-sound-volume").type(TypeToken.get(Float.class)) + .min(0.0F).max(1.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + + private ModKnockbackTrainer() { + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModLightOverlay.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModLightOverlay.java new file mode 100644 index 00000000..07d415e1 --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModLightOverlay.java @@ -0,0 +1,208 @@ +/* + * 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.mods.impl; + +import com.lunarclient.apollo.option.NumberOption; +import com.lunarclient.apollo.option.SimpleOption; +import io.leangen.geantyref.TypeToken; +import java.awt.Color; + +/** + * Shows the light levels of blocks where mobs can spawn. + * + * @since 1.2.9 + */ +public final class ModLightOverlay { + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption ENABLED = SimpleOption.builder() + .node("light-overlay", "enabled").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * The rendering limit (in chunks) for light overlay values. Higher values might lower your FPS. + * + * @since 1.2.9 + */ + public static final NumberOption RENDER_RANGE_LIMIT = NumberOption.number() + .comment("The rendering limit (in chunks) for light overlay values. Higher values might lower your FPS.") + .node("light-overlay", "render-range-limit").type(TypeToken.get(Integer.class)) + .min(1).max(12) + .defaultValue(2) + .notifyClient() + .build(); + + /** + * With this option on, the overlay updates aren't deferred, and happen as fast as possible.This looks nicer (less "pop in"), but turning this option on comes at a potential performance cost. + * + * @since 1.2.9 + */ + public static final SimpleOption FAST_UPDATES = SimpleOption.builder() + .comment("With this option on, the overlay updates aren't deferred, and happen as fast as possible.This looks nicer (less \"pop in\"), but turning this option on comes at a potential performance cost.") + .node("light-overlay", "fast-updates").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Reduce the amount of overlays that are rendered, increasing performance, especially at higher rendering ranges.This comes at the expense of overlays at the edges of the screen sometimes popping in and out as you look around.It's highly recommended that this option is kept on, especially if using higher rendering ranges. + * + * @since 1.2.9 + */ + public static final SimpleOption CULLING = SimpleOption.builder() + .comment("Reduce the amount of overlays that are rendered, increasing performance, especially at higher rendering ranges.This comes at the expense of overlays at the edges of the screen sometimes popping in and out as you look around.It's highly recommended that this option is kept on, especially if using higher rendering ranges.") + .node("light-overlay", "culling").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * With this option on, the real, effective light value is displayed. With this option off, only the block light is used, completely ignoring the sky/day light. + * + * @since 1.2.9 + */ + public static final SimpleOption INCLUDE_SKY_LIGHT = SimpleOption.builder() + .comment("With this option on, the real, effective light value is displayed. With this option off, only the block light is used, completely ignoring the sky/day light.") + .node("light-overlay", "include-sky-light").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * Only show on blocks which have light levels that hostile mobs can spawn on. + * + * @since 1.2.9 + */ + public static final SimpleOption HIDE_UNSPAWNABLE_LIGHT = SimpleOption.builder() + .comment("Only show on blocks which have light levels that hostile mobs can spawn on") + .node("light-overlay", "hide-unspawnable-light").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * Set a custom threshold for Light Check. This overrides the vanilla hostile mob spawning value, and shows the light overlay on all blocks on which the light level falls below this threshold. + * + * @since 1.2.9 + */ + public static final SimpleOption CUSTOM_LIGHT_THRESHOLD = SimpleOption.builder() + .comment("Set a custom threshold for Light Check. This overrides the vanilla hostile mob spawning value, and shows the light overlay on all blocks on which the light level falls below this threshold") + .node("light-overlay", "custom-light-threshold").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption THRESHOLD = NumberOption.number() + .node("light-overlay", "threshold").type(TypeToken.get(Integer.class)) + .min(0).max(15) + .defaultValue(15) + .notifyClient() + .build(); + + /** + * Show a number for the light value of each block.Turning this option on might lower your FPS. + * + * @since 1.2.9 + */ + public static final SimpleOption SHOW_LIGHT_VALUE = SimpleOption.builder() + .comment("Show a number for the light value of each block.Turning this option on might lower your FPS.") + .node("light-overlay", "show-light-value").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption CROSS_THICKNESS = NumberOption.number() + .node("light-overlay", "cross-thickness").type(TypeToken.get(Float.class)) + .min(0.5F).max(10.0F) + .defaultValue(2.0F) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption TEXT_COLOR = SimpleOption.builder() + .node("light-overlay", "text-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) + .notifyClient() + .build(); + + /** + * Color where hostile mobs can't spawn. + * + * @since 1.2.9 + */ + public static final SimpleOption BRIGHT_COLOR = SimpleOption.builder() + .comment("Color where hostile mobs can't spawn") + .node("light-overlay", "bright-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 255, 0)) + .notifyClient() + .build(); + + /** + * Color where hostile mobs can spawn. + * + * @since 1.2.9 + */ + public static final SimpleOption DARK_COLOR = SimpleOption.builder() + .comment("Color where hostile mobs can spawn") + .node("light-overlay", "dark-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 0, 0)) + .notifyClient() + .build(); + + /** + * When this is off, the color is picked based on the light level allowing hostile mobs to spawn. When this is on, the color is interpolated smoothly. + * + * @since 1.2.9 + */ + public static final SimpleOption LIGHT_OVERLAY_DYNAMIC_COLOR = SimpleOption.builder() + .comment("When this is off, the color is picked based on the light level allowing hostile mobs to spawn. When this is on, the color is interpolated smoothly.") + .node("light-overlay", "light-overlay-dynamic-color").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + private ModLightOverlay() { + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModNametag.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModNametag.java index 64e724b6..1ab58554 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModNametag.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModNametag.java @@ -104,6 +104,18 @@ public final class ModNametag { .notifyClient() .build(); + /** + * With this option enabled, your nametag will be hidden in third person when there is a passenger attached to you. + * + * @since 1.2.9 + */ + public static final SimpleOption HIDE_NAMETAG_F5PASSENGERS = SimpleOption.builder() + .comment("With this option enabled, your nametag will be hidden in third person when there is a passenger attached to you.") + .node("nametag", "hide-nametag-f5-passengers").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + /** * No documentation available. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModOverlayMod.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModOverlayMod.java index 21f6786f..b04034dd 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModOverlayMod.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModOverlayMod.java @@ -70,6 +70,18 @@ public final class ModOverlayMod { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption FIRE_BLOCK_HEIGHT = NumberOption.number() + .node("overlay-mod", "fire-block-height").type(TypeToken.get(Float.class)) + .min(0.0F).max(2.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + /** * No documentation available. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPing.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPing.java index 059e6312..3e46e2bf 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPing.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPing.java @@ -47,15 +47,15 @@ public final class ModPing { .build(); /** - * Faster updates may impact performance. + * How often ping is measured. If you have trouble connecting to a server, try increasing this. * * @since 1.0.0 */ public static final NumberOption UPDATE_INTERVAL_SEC = NumberOption.number() - .comment("Faster updates may impact performance") + .comment("How often ping is measured. If you have trouble connecting to a server, try increasing this.") .node("ping", "update-interval-sec").type(TypeToken.get(Integer.class)) .min(1).max(120) - .defaultValue(20) + .defaultValue(5) .notifyClient() .build(); diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionCounter.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionCounter.java new file mode 100644 index 00000000..02dac49d --- /dev/null +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionCounter.java @@ -0,0 +1,213 @@ +/* + * 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.mods.impl; + +import com.lunarclient.apollo.option.NumberOption; +import com.lunarclient.apollo.option.SimpleOption; +import io.leangen.geantyref.TypeToken; +import java.awt.Color; + +/** + * Displays how many healing potions or soups are currently in your inventory on the HUD. + * + * @since 1.2.9 + */ +public final class ModPotionCounter { + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption ENABLED = SimpleOption.builder() + .node("potion-counter", "enabled").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption SCALE = NumberOption.number() + .node("potion-counter", "scale").type(TypeToken.get(Float.class)) + .min(0.25F).max(5.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + + /** + * Adds a shadow to text. + * + * @since 1.2.9 + */ + public static final SimpleOption TEXT_SHADOW = SimpleOption.builder() + .comment("Adds a shadow to text") + .node("potion-counter", "text-shadow").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BRACKETS = SimpleOption.builder() + .node("potion-counter", "brackets").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BRACKET_COLOR = SimpleOption.builder() + .node("potion-counter", "bracket-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BACKGROUND = SimpleOption.builder() + .node("potion-counter", "background").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * If this is disabled the background will change size with the text. + * + * @since 1.2.9 + */ + public static final SimpleOption STATIC_BACKGROUND_WIDTH = SimpleOption.builder() + .comment("If this is disabled the background will change size with the text.") + .node("potion-counter", "static-background-width").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * If this is disabled the background will change size with the text. + * + * @since 1.2.9 + */ + public static final SimpleOption STATIC_BACKGROUND_HEIGHT = SimpleOption.builder() + .comment("If this is disabled the background will change size with the text.") + .node("potion-counter", "static-background-height").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption BACKGROUND_WIDTH = NumberOption.number() + .node("potion-counter", "background-width").type(TypeToken.get(Integer.class)) + .min(40).max(62) + .defaultValue(56) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption BACKGROUND_HEIGHT = NumberOption.number() + .node("potion-counter", "background-height").type(TypeToken.get(Integer.class)) + .min(10).max(22) + .defaultValue(18) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BORDER = SimpleOption.builder() + .node("potion-counter", "border").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption BORDER_THICKNESS = NumberOption.number() + .node("potion-counter", "border-thickness").type(TypeToken.get(Float.class)) + .min(0.5F).max(3.0F) + .defaultValue(0.5F) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BORDER_COLOR = SimpleOption.builder() + .node("potion-counter", "border-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 0, 0, 159)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption BACKGROUND_COLOR = SimpleOption.builder() + .node("potion-counter", "background-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(0, 0, 0, 111)) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption TEXT_COLOR = SimpleOption.builder() + .node("potion-counter", "text-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) + .notifyClient() + .build(); + + private ModPotionCounter() { + } + +} diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionEffects.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionEffects.java index 93be05cc..649d2ee1 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionEffects.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModPotionEffects.java @@ -49,22 +49,22 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final NumberOption SCALE = NumberOption.number() - .node("potion-effects", "scale").type(TypeToken.get(Float.class)) - .min(0.25F).max(5.0F) - .defaultValue(1.0F) + public static final SimpleOption HIDE_AMBIENT_DURATION = SimpleOption.builder() + .node("potion-effects", "hide-ambient-duration").type(TypeToken.get(Boolean.class)) + .defaultValue(false) .notifyClient() .build(); /** - * No documentation available. + * Put the good effects and the bad effects on separate lines. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final SimpleOption SHOW_IN_INVENTORY = SimpleOption.builder() - .node("potion-effects", "show-in-inventory").type(TypeToken.get(Boolean.class)) + public static final SimpleOption VANILLA_GROUP_EFFECTS = SimpleOption.builder() + .comment("Put the good effects and the bad effects on separate lines") + .node("potion-effects", "vanilla-group-effects").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); @@ -72,14 +72,49 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final SimpleOption SHOW_WHILE_TYPING = SimpleOption.builder() - .node("potion-effects", "show-while-typing").type(TypeToken.get(Boolean.class)) + public static final SimpleOption VANILLA_MODE_HORIZONTAL = SimpleOption.builder() + .node("potion-effects", "vanilla-mode-horizontal").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption VANILLA_MODE_TILES_PER_LINE = NumberOption.number() + .node("potion-effects", "vanilla-mode-tiles-per-line").type(TypeToken.get(Integer.class)) + .min(1).max(10) + .defaultValue(10) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.1.9 + */ + public static final SimpleOption MINIMAL_MODE_HORIZONTAL = SimpleOption.builder() + .node("potion-effects", "minimal-mode-horizontal").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption MINIMAL_MODE_TILES_PER_LINE = NumberOption.number() + .node("potion-effects", "minimal-mode-tiles-per-line").type(TypeToken.get(Integer.class)) + .min(1).max(10) + .defaultValue(4) + .notifyClient() + .build(); + /** * No documentation available. * @@ -92,13 +127,12 @@ public final class ModPotionEffects { .build(); /** - * Adds a shadow to text. + * No documentation available. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final SimpleOption TEXT_SHADOW = SimpleOption.builder() - .comment("Adds a shadow to text") - .node("potion-effects", "text-shadow").type(TypeToken.get(Boolean.class)) + public static final SimpleOption EFFECT_DURATION = SimpleOption.builder() + .node("potion-effects", "effect-duration").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); @@ -106,10 +140,10 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.1.9 + * @since 1.2.9 */ - public static final SimpleOption BACKGROUND = SimpleOption.builder() - .node("potion-effects", "background").type(TypeToken.get(Boolean.class)) + public static final SimpleOption EFFECT_AMPLIFIER = SimpleOption.builder() + .node("potion-effects", "effect-amplifier").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); @@ -117,32 +151,44 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.1.9 + * @since 1.0.0 */ - public static final SimpleOption MINIMAL_MODE = SimpleOption.builder() - .node("potion-effects", "minimal-mode").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + public static final SimpleOption SHOW_IN_INVENTORY = SimpleOption.builder() + .node("potion-effects", "show-in-inventory").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); /** * No documentation available. * - * @since 1.1.9 + * @since 1.0.0 */ - public static final SimpleOption MINIMAL_MODE_HORIZONTAL = SimpleOption.builder() - .node("potion-effects", "minimal-mode-horizontal").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + public static final SimpleOption SHOW_WHILE_TYPING = SimpleOption.builder() + .node("potion-effects", "show-while-typing").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); /** * No documentation available. * - * @since 1.1.9 + * @since 1.0.0 */ - public static final SimpleOption BORDER = SimpleOption.builder() - .node("potion-effects", "border").type(TypeToken.get(Boolean.class)) + public static final SimpleOption POTION_BLINK = SimpleOption.builder() + .node("potion-effects", "potion-blink").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + + /** + * With this enabled, the potion effect icon blinks as well. + * + * @since 1.2.9 + */ + public static final SimpleOption POTION_BLINK_ICON = SimpleOption.builder() + .comment("With this enabled, the potion effect icon blinks as well") + .node("potion-effects", "potion-blink-icon").type(TypeToken.get(Boolean.class)) .defaultValue(false) .notifyClient() .build(); @@ -150,23 +196,23 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.1.9 + * @since 1.0.0 */ - public static final NumberOption BORDER_THICKNESS = NumberOption.number() - .node("potion-effects", "border-thickness").type(TypeToken.get(Float.class)) - .min(0.5F).max(3.0F) - .defaultValue(0.5F) + public static final NumberOption BLINK_DURATION = NumberOption.number() + .node("potion-effects", "blink-duration").type(TypeToken.get(Integer.class)) + .min(2).max(20) + .defaultValue(10) .notifyClient() .build(); /** * No documentation available. * - * @since 1.1.9 + * @since 1.2.9 */ - public static final SimpleOption FORMATTED_DURATIONS = SimpleOption.builder() - .node("potion-effects", "formatted-durations").type(TypeToken.get(Boolean.class)) - .defaultValue(false) + public static final SimpleOption VANILLA_BLINK_COLOR = SimpleOption.builder() + .node("potion-effects", "vanilla-blink-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) .notifyClient() .build(); @@ -192,6 +238,17 @@ public final class ModPotionEffects { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.1.9 + */ + public static final SimpleOption FORMATTED_DURATIONS = SimpleOption.builder() + .node("potion-effects", "formatted-durations").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + /** * No documentation available. * @@ -204,25 +261,25 @@ public final class ModPotionEffects { .build(); /** - * No documentation available. + * Completely hide the potion effects mod HUD. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final SimpleOption POTION_BLINK = SimpleOption.builder() - .node("potion-effects", "potion-blink").type(TypeToken.get(Boolean.class)) - .defaultValue(true) + public static final SimpleOption HIDE_POTION_STATUS = SimpleOption.builder() + .comment("Completely hide the potion effects mod HUD") + .node("potion-effects", "hide-potion-status").type(TypeToken.get(Boolean.class)) + .defaultValue(false) .notifyClient() .build(); /** * No documentation available. * - * @since 1.0.0 + * @since 1.1.9 */ - public static final NumberOption BLINK_DURATION = NumberOption.number() - .node("potion-effects", "blink-duration").type(TypeToken.get(Integer.class)) - .min(2).max(20) - .defaultValue(10) + public static final SimpleOption BACKGROUND = SimpleOption.builder() + .node("potion-effects", "background").type(TypeToken.get(Boolean.class)) + .defaultValue(true) .notifyClient() .build(); @@ -237,6 +294,29 @@ public final class ModPotionEffects { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.1.9 + */ + public static final SimpleOption BORDER = SimpleOption.builder() + .node("potion-effects", "border").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.1.9 + */ + public static final NumberOption BORDER_THICKNESS = NumberOption.number() + .node("potion-effects", "border-thickness").type(TypeToken.get(Float.class)) + .min(0.5F).max(3.0F) + .defaultValue(0.5F) + .notifyClient() + .build(); + /** * No documentation available. * @@ -248,6 +328,18 @@ public final class ModPotionEffects { .notifyClient() .build(); + /** + * Adds a shadow to text. + * + * @since 1.0.0 + */ + public static final SimpleOption TEXT_SHADOW = SimpleOption.builder() + .comment("Adds a shadow to text") + .node("potion-effects", "text-shadow").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + /** * No documentation available. * @@ -259,6 +351,28 @@ public final class ModPotionEffects { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_INFO_BASED_ON_EFFECT = SimpleOption.builder() + .node("potion-effects", "color-info-based-on-effect").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption SHOW_EFFECT_BACKGROUND = SimpleOption.builder() + .node("potion-effects", "show-effect-background").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + /** * No documentation available. * @@ -273,14 +387,50 @@ public final class ModPotionEffects { /** * No documentation available. * - * @since 1.0.0 + * @since 1.2.9 */ - public static final SimpleOption DURATION_COLOR = SimpleOption.builder() - .node("potion-effects", "duration-color").type(TypeToken.get(Color.class)) + public static final SimpleOption INFO_COLOR = SimpleOption.builder() + .node("potion-effects", "info-color").type(TypeToken.get(Color.class)) .defaultValue(new Color(255, 255, 255)) .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption VANILLA_ICON_SCALE = NumberOption.number() + .node("potion-effects", "vanilla-icon-scale").type(TypeToken.get(Float.class)) + .min(0.5F).max(1.25F) + .defaultValue(1.0F) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final NumberOption VANILLA_TEXT_SCALE = NumberOption.number() + .node("potion-effects", "vanilla-text-scale").type(TypeToken.get(Float.class)) + .min(0.25F).max(2.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.0.0 + */ + public static final NumberOption SCALE = NumberOption.number() + .node("potion-effects", "scale").type(TypeToken.get(Float.class)) + .min(0.25F).max(5.0F) + .defaultValue(1.0F) + .notifyClient() + .build(); + /** * No documentation available. * @@ -545,6 +695,30 @@ public final class ModPotionEffects { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.1.9 + */ + @Deprecated + public static final SimpleOption MINIMAL_MODE = SimpleOption.builder() + .node("potion-effects", "minimal-mode").type(TypeToken.get(Boolean.class)) + .defaultValue(false) + .notifyClient() + .build(); + + /** + * No documentation available. + * + * @since 1.0.0 + */ + @Deprecated + public static final SimpleOption DURATION_COLOR = SimpleOption.builder() + .node("potion-effects", "duration-color").type(TypeToken.get(Color.class)) + .defaultValue(new Color(255, 255, 255)) + .notifyClient() + .build(); + private ModPotionEffects() { } diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScoreboard.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScoreboard.java index b0425df7..21a39371 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScoreboard.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScoreboard.java @@ -115,6 +115,17 @@ public final class ModScoreboard { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption DISPLAY_TOGGLE_MESSAGE = SimpleOption.builder() + .node("scoreboard", "display-toggle-message").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + /** * No documentation available. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScreenshot.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScreenshot.java index 2945c34b..9859a34b 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScreenshot.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModScreenshot.java @@ -88,6 +88,17 @@ public final class ModScreenshot { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption DELETE_OPTION = SimpleOption.builder() + .node("screenshot", "delete-option").type(TypeToken.get(Boolean.class)) + .defaultValue(true) + .notifyClient() + .build(); + /** * A screenshot file saved with world details enabled has metadata that contains your player coordinates, the biome, and more. * diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModSkyblock.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModSkyblock.java index e5ca424d..c46082b9 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModSkyblock.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModSkyblock.java @@ -59,25 +59,25 @@ public final class ModSkyblock { .build(); /** - * Adds tab completion to the /warp command. + * Fixes being unable to use the "Pick Block" keybind on items when it is bound to a mouse button. * - * @since 1.2.2 + * @since 1.2.5 */ - public static final SimpleOption SKYBLOCK_AUTOCOMPLETE_WARPS = SimpleOption.builder() - .comment("Adds tab completion to the /warp command.") - .node("skyblock", "skyblock-autocomplete-warps").type(TypeToken.get(Boolean.class)) + public static final SimpleOption MIDDLE_CLICK_ARMOR_FIX = SimpleOption.builder() + .comment("Fixes being unable to use the \"Pick Block\" keybind on items when it is bound to a mouse button.") + .node("skyblock", "middle-click-armor-fix").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); /** - * Fixes being unable to use the "Pick Block" keybind on items when it is bound to a mouse button. + * Adds tab completion to the /warp command. * - * @since 1.2.5 + * @since 1.2.2 */ - public static final SimpleOption MIDDLE_CLICK_ARMOR_FIX = SimpleOption.builder() - .comment("Fixes being unable to use the \"Pick Block\" keybind on items when it is bound to a mouse button.") - .node("skyblock", "middle-click-armor-fix").type(TypeToken.get(Boolean.class)) + public static final SimpleOption SKYBLOCK_AUTOCOMPLETE_WARPS = SimpleOption.builder() + .comment("Adds tab completion to the /warp command.") + .node("skyblock", "skyblock-autocomplete-warps").type(TypeToken.get(Boolean.class)) .defaultValue(true) .notifyClient() .build(); @@ -204,7 +204,7 @@ public final class ModSkyblock { public static final NumberOption CUSTOM_CHIME_VOLUME = NumberOption.number() .node("skyblock", "custom-chime-volume").type(TypeToken.get(Float.class)) .min(0.0F).max(1.0F) - .defaultValue(5.0F) + .defaultValue(0.5F) .notifyClient() .build(); diff --git a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModTierTagger.java b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModTierTagger.java index 87d3bfa8..a751c027 100644 --- a/api/src/main/java/com/lunarclient/apollo/mods/impl/ModTierTagger.java +++ b/api/src/main/java/com/lunarclient/apollo/mods/impl/ModTierTagger.java @@ -28,7 +28,7 @@ import java.awt.Color; /** - * Show player's PvP tier on their Name Tag. + * Show player's PvP tier on their Name Tag. Also has a /lctiers command to look up player tiers. * * @since 1.1.9 */ @@ -122,6 +122,17 @@ public final class ModTierTagger { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_MT1 = SimpleOption.builder() + .node("tier-tagger", "color-m-t1").type(TypeToken.get(Color.class)) + .defaultValue(new Color(234, 193, 79)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -144,6 +155,17 @@ public final class ModTierTagger { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_MT2 = SimpleOption.builder() + .node("tier-tagger", "color-m-t2").type(TypeToken.get(Color.class)) + .defaultValue(new Color(150, 160, 174)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -166,6 +188,17 @@ public final class ModTierTagger { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_MT3 = SimpleOption.builder() + .node("tier-tagger", "color-m-t3").type(TypeToken.get(Color.class)) + .defaultValue(new Color(200, 120, 60)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -188,6 +221,17 @@ public final class ModTierTagger { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_MT4 = SimpleOption.builder() + .node("tier-tagger", "color-m-t4").type(TypeToken.get(Color.class)) + .defaultValue(new Color(101, 91, 121)) + .notifyClient() + .build(); + /** * No documentation available. * @@ -210,6 +254,17 @@ public final class ModTierTagger { .notifyClient() .build(); + /** + * No documentation available. + * + * @since 1.2.9 + */ + public static final SimpleOption COLOR_MT5 = SimpleOption.builder() + .node("tier-tagger", "color-m-t5").type(TypeToken.get(Color.class)) + .defaultValue(new Color(101, 91, 121)) + .notifyClient() + .build(); + /** * No documentation available. * diff --git a/docs/developers/mods/_meta.json b/docs/developers/mods/_meta.json index 5729f816..1be56200 100644 --- a/docs/developers/mods/_meta.json +++ b/docs/developers/mods/_meta.json @@ -1,6 +1,7 @@ { "2ditems": "2dItems", "3dskins": "3dSkins", + "actionbar": "ActionBar", "armorstatus": "Armorstatus", "attackindicator": "AttackIndicator", "audiosubtitles": "AudioSubtitles", @@ -40,7 +41,9 @@ "itemtracker": "ItemTracker", "keystrokes": "Keystrokes", "killsounds": "KillSounds", + "knockbacktrainer": "KnockbackTrainer", "lighting": "Lighting", + "lightoverlay": "LightOverlay", "markers": "Markers", "memory": "Memory", "menublur": "MenuBlur", @@ -58,6 +61,7 @@ "particlechanger": "ParticleChanger", "ping": "Ping", "playtime": "Playtime", + "potioncounter": "PotionCounter", "potioneffects": "PotionEffects", "pvpinfo": "PvpInfo", "quickplay": "Quickplay", diff --git a/docs/developers/mods/actionbar.mdx b/docs/developers/mods/actionbar.mdx new file mode 100644 index 00000000..2717694f --- /dev/null +++ b/docs/developers/mods/actionbar.mdx @@ -0,0 +1,31 @@ +# Action Bar + +Allows you to move and scale Minecraft's action bar. + +## Integration + +### How to toggle the mod + +```java +public void toggleActionBarExample(Player viewer, boolean value) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.getOptions().set(apolloPlayer, ModActionBar.ENABLED, value)); +} +``` + +## Available options + +- __`ENABLED`__ + - Config Key: `enabled` + - Values + - Type: `Boolean` + - Default: `false` + +- __`SCALE`__ + - Config Key: `scale` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.25F` + - Maximum: `5.0F` + diff --git a/docs/developers/mods/chat.mdx b/docs/developers/mods/chat.mdx index d3382e81..bf9fdc98 100644 --- a/docs/developers/mods/chat.mdx +++ b/docs/developers/mods/chat.mdx @@ -115,6 +115,41 @@ For example, if your name is Notch, this will ping on Notch but not Notch123. - Type: `Boolean` - Default: `false` +- __`CHAT_HEADS`__ + - Displays the player's Minecraft head before their message in chat. + - Config Key: `chat-heads` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CHAT_HEADS_FETCH_UNKNOWN`__ + - Looks up skins for chat senders that aren't in the tab list, like players on other servers of a network. Head detection may be less accurate. + - Config Key: `chat-heads-fetch-unknown` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CHAT_EMOJI`__ + - Adds emojis to chat. Shortcodes like :smile: autocomplete as you type, and emojis in messages show as colorful Twemoji. + - Config Key: `chat-emoji` + - Values + - Type: `Boolean` + - Default: `true` + +- __`CHAT_EMOJI_UNICODE_ONLY`__ + - Shows emojis with Minecraft's built-in font instead of Twemoji images. Shortcodes the game font can't display are left as text. + - Config Key: `chat-emoji-unicode-only` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CHAT_EMOJI_CONVERT_SHORTCODES`__ + - Turns shortcodes like :skull: in other players' messages into emojis. When disabled, only emojis sent as actual characters are shown. + - Config Key: `chat-emoji-convert-shortcodes` + - Values + - Type: `Boolean` + - Default: `true` + - __`COPY_CHAT`__ - Copies the hovered chat message when holding the keybind and clicking. - Config Key: `copy-chat` diff --git a/docs/developers/mods/hitbox.mdx b/docs/developers/mods/hitbox.mdx index 6f91d4fc..6fc2c955 100644 --- a/docs/developers/mods/hitbox.mdx +++ b/docs/developers/mods/hitbox.mdx @@ -56,6 +56,37 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_PLAYER_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-player-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PLAYER_HITTABLE_COLOR`__ + - Config Key: `hitbox-player-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_PLAYER_ONLY_SHOW_HITTABLE`__ + - Only show the hitbox when the entity is within reach and under your crosshair + - Config Key: `hitbox-player-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PLAYER_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-player-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PLAYER_DAMAGED_COLOR`__ + - Config Key: `hitbox-player-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_PLAYER_LOOK_VECTOR`__ - Config Key: `hitbox-player-look-vector` - Values @@ -82,6 +113,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_ITEM_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-item-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_HITTABLE_COLOR`__ + - Config Key: `hitbox-item-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_ITEM_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-item-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-item-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_DAMAGED_COLOR`__ + - Config Key: `hitbox-item-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_ITEM_LOOK_VECTOR`__ - Config Key: `hitbox-item-look-vector` - Values @@ -108,6 +169,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_EXP_ORB_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-exp-orb-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_EXP_ORB_HITTABLE_COLOR`__ + - Config Key: `hitbox-exp-orb-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_EXP_ORB_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-exp-orb-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_EXP_ORB_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-exp-orb-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_EXP_ORB_DAMAGED_COLOR`__ + - Config Key: `hitbox-exp-orb-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_EXP_ORB_LOOK_VECTOR`__ - Config Key: `hitbox-exp-orb-look-vector` - Values @@ -134,6 +225,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_ITEM_FRAME_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-item-frame-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_FRAME_HITTABLE_COLOR`__ + - Config Key: `hitbox-item-frame-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_ITEM_FRAME_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-item-frame-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_FRAME_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-item-frame-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ITEM_FRAME_DAMAGED_COLOR`__ + - Config Key: `hitbox-item-frame-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_ITEM_FRAME_LOOK_VECTOR`__ - Config Key: `hitbox-item-frame-look-vector` - Values @@ -160,6 +281,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_FIREWORK_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-firework-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREWORK_HITTABLE_COLOR`__ + - Config Key: `hitbox-firework-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_FIREWORK_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-firework-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREWORK_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-firework-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREWORK_DAMAGED_COLOR`__ + - Config Key: `hitbox-firework-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_FIREWORK_LOOK_VECTOR`__ - Config Key: `hitbox-firework-look-vector` - Values @@ -186,6 +337,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_WITHER_SKULL_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-wither-skull-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_WITHER_SKULL_HITTABLE_COLOR`__ + - Config Key: `hitbox-wither-skull-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_WITHER_SKULL_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-wither-skull-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_WITHER_SKULL_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-wither-skull-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_WITHER_SKULL_DAMAGED_COLOR`__ + - Config Key: `hitbox-wither-skull-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_WITHER_SKULL_LOOK_VECTOR`__ - Config Key: `hitbox-wither-skull-look-vector` - Values @@ -212,6 +393,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_SNOWBALL_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-snowball-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_SNOWBALL_HITTABLE_COLOR`__ + - Config Key: `hitbox-snowball-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_SNOWBALL_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-snowball-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_SNOWBALL_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-snowball-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_SNOWBALL_DAMAGED_COLOR`__ + - Config Key: `hitbox-snowball-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_SNOWBALL_LOOK_VECTOR`__ - Config Key: `hitbox-snowball-look-vector` - Values @@ -238,6 +449,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_FIREBALL_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-fireball-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREBALL_HITTABLE_COLOR`__ + - Config Key: `hitbox-fireball-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_FIREBALL_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-fireball-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREBALL_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-fireball-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_FIREBALL_DAMAGED_COLOR`__ + - Config Key: `hitbox-fireball-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_FIREBALL_LOOK_VECTOR`__ - Config Key: `hitbox-fireball-look-vector` - Values @@ -264,6 +505,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_ARROW_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-arrow-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ARROW_HITTABLE_COLOR`__ + - Config Key: `hitbox-arrow-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_ARROW_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-arrow-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ARROW_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-arrow-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_ARROW_DAMAGED_COLOR`__ + - Config Key: `hitbox-arrow-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_ARROW_LOOK_VECTOR`__ - Config Key: `hitbox-arrow-look-vector` - Values @@ -290,6 +561,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_PROJECTILE_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-projectile-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PROJECTILE_HITTABLE_COLOR`__ + - Config Key: `hitbox-projectile-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_PROJECTILE_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-projectile-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PROJECTILE_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-projectile-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PROJECTILE_DAMAGED_COLOR`__ + - Config Key: `hitbox-projectile-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_PROJECTILE_LOOK_VECTOR`__ - Config Key: `hitbox-projectile-look-vector` - Values @@ -316,6 +617,37 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_MONSTER_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-monster-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_MONSTER_HITTABLE_COLOR`__ + - Config Key: `hitbox-monster-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_MONSTER_ONLY_SHOW_HITTABLE`__ + - Only show the hitbox when the entity is within reach and under your crosshair + - Config Key: `hitbox-monster-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_MONSTER_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-monster-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_MONSTER_DAMAGED_COLOR`__ + - Config Key: `hitbox-monster-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_MONSTER_LOOK_VECTOR`__ - Config Key: `hitbox-monster-look-vector` - Values @@ -342,6 +674,37 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_PASSIVE_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-passive-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PASSIVE_HITTABLE_COLOR`__ + - Config Key: `hitbox-passive-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_PASSIVE_ONLY_SHOW_HITTABLE`__ + - Only show the hitbox when the entity is within reach and under your crosshair + - Config Key: `hitbox-passive-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PASSIVE_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-passive-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_PASSIVE_DAMAGED_COLOR`__ + - Config Key: `hitbox-passive-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_PASSIVE_LOOK_VECTOR`__ - Config Key: `hitbox-passive-look-vector` - Values @@ -368,6 +731,36 @@ public void toggleHitboxExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFFFFF` +- __`HITBOX_OTHER_SHOW_HITTABLE_COLOR`__ + - Config Key: `hitbox-other-show-hittable-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_OTHER_HITTABLE_COLOR`__ + - Config Key: `hitbox-other-hittable-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`HITBOX_OTHER_ONLY_SHOW_HITTABLE`__ + - Config Key: `hitbox-other-only-show-hittable` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_OTHER_SHOW_DAMAGED_COLOR`__ + - Config Key: `hitbox-other-show-damaged-color` + - Values + - Type: `Boolean` + - Default: `false` + +- __`HITBOX_OTHER_DAMAGED_COLOR`__ + - Config Key: `hitbox-other-damaged-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + - __`HITBOX_OTHER_LOOK_VECTOR`__ - Config Key: `hitbox-other-look-vector` - Values diff --git a/docs/developers/mods/knockbacktrainer.mdx b/docs/developers/mods/knockbacktrainer.mdx new file mode 100644 index 00000000..8dfc098c --- /dev/null +++ b/docs/developers/mods/knockbacktrainer.mdx @@ -0,0 +1,53 @@ +# Knockback Trainer + +Train jump resets: how close your jump press lands to the tick you were hit. + +## Integration + +### How to toggle the mod + +```java +public void toggleKnockbackTrainerExample(Player viewer, boolean value) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.getOptions().set(apolloPlayer, ModKnockbackTrainer.ENABLED, value)); +} +``` + +## Available options + +- __`ENABLED`__ + - Config Key: `enabled` + - Values + - Type: `Boolean` + - Default: `false` + +- __`TARGET_TICKS`__ + - Config Key: `target-ticks` + - Values + - Type: `Integer` + - Default: `0` + - Minimum: `0` + - Maximum: `6` + +- __`WINDOW_TICKS`__ + - Config Key: `window-ticks` + - Values + - Type: `Integer` + - Default: `5` + - Minimum: `0` + - Maximum: `10` + +- __`FALLING_HIT_SOUND`__ + - Config Key: `falling-hit-sound` + - Values + - Type: `Boolean` + - Default: `true` + +- __`FALLING_HIT_SOUND_VOLUME`__ + - Config Key: `falling-hit-sound-volume` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.0F` + - Maximum: `1.0F` + diff --git a/docs/developers/mods/lightoverlay.mdx b/docs/developers/mods/lightoverlay.mdx new file mode 100644 index 00000000..9350a15b --- /dev/null +++ b/docs/developers/mods/lightoverlay.mdx @@ -0,0 +1,121 @@ +# Light Overlay + +Shows the light levels of blocks where mobs can spawn + +## Integration + +### How to toggle the mod + +```java +public void toggleLightOverlayExample(Player viewer, boolean value) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.getOptions().set(apolloPlayer, ModLightOverlay.ENABLED, value)); +} +``` + +## Available options + +- __`ENABLED`__ + - Config Key: `enabled` + - Values + - Type: `Boolean` + - Default: `false` + +- __`RENDER_RANGE_LIMIT`__ + - The rendering limit (in chunks) for light overlay values. Β§eHigher values might lower your FPS. + - Config Key: `render-range-limit` + - Values + - Type: `Integer` + - Default: `2` + - Minimum: `1` + - Maximum: `12` + +- __`FAST_UPDATES`__ + - With this option on, the overlay updates aren't deferred, and happen as fast as possible. +This looks nicer (less "pop in"), but Β§eturning this option on comes at a potential performance cost. + - Config Key: `fast-updates` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CULLING`__ + - Reduce the amount of overlays that are rendered, increasing performance, especially at higher rendering ranges. +This comes at the expense of overlays at the edges of the screen sometimes popping in and out as you look around. +It's highly recommended that this option is kept on, especially if using higher rendering ranges. + - Config Key: `culling` + - Values + - Type: `Boolean` + - Default: `true` + +- __`INCLUDE_SKY_LIGHT`__ + - With this option on, the real, effective light value is displayed. With this option off, only the block light is used, completely ignoring the sky/day light. + - Config Key: `include-sky-light` + - Values + - Type: `Boolean` + - Default: `true` + +- __`HIDE_UNSPAWNABLE_LIGHT`__ + - Only show on blocks which have light levels that hostile mobs can spawn on + - Config Key: `hide-unspawnable-light` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CUSTOM_LIGHT_THRESHOLD`__ + - Set a custom threshold for Light Check. This overrides the vanilla hostile mob spawning value, and shows the light overlay on all blocks on which the light level falls below this threshold + - Config Key: `custom-light-threshold` + - Values + - Type: `Boolean` + - Default: `false` + +- __`THRESHOLD`__ + - Config Key: `threshold` + - Values + - Type: `Integer` + - Default: `15` + - Minimum: `0` + - Maximum: `15` + +- __`SHOW_LIGHT_VALUE`__ + - Show a number for the light value of each block. +Turning this option on might lower your FPS. + - Config Key: `show-light-value` + - Values + - Type: `Boolean` + - Default: `false` + +- __`CROSS_THICKNESS`__ + - Config Key: `cross-thickness` + - Values + - Type: `Float` + - Default: `2.0F` + - Minimum: `0.5F` + - Maximum: `10.0F` + +- __`TEXT_COLOR`__ + - Config Key: `text-color` + - Values + - Type: `String` + - Default: `#FFFFFFFF` + +- __`BRIGHT_COLOR`__ + - Color where hostile mobs can't spawn + - Config Key: `bright-color` + - Values + - Type: `String` + - Default: `#FF00FF00` + +- __`DARK_COLOR`__ + - Color where hostile mobs can spawn + - Config Key: `dark-color` + - Values + - Type: `String` + - Default: `#FFFF0000` + +- __`LIGHT_OVERLAY_DYNAMIC_COLOR`__ + - When this is off, the color is picked based on the light level allowing hostile mobs to spawn. When this is on, the color is interpolated smoothly. + - Config Key: `light-overlay-dynamic-color` + - Values + - Type: `Boolean` + - Default: `false` + diff --git a/docs/developers/mods/nametag.mdx b/docs/developers/mods/nametag.mdx index ec4830db..5f17cb2d 100644 --- a/docs/developers/mods/nametag.mdx +++ b/docs/developers/mods/nametag.mdx @@ -55,6 +55,13 @@ public void toggleNametagsExample(Player viewer, boolean value) { - Minimum: `0.0F` - Maximum: `1.0F` +- __`HIDE_NAMETAG_F5PASSENGERS`__ + - With this option enabled, your nametag will be hidden in third person when there is a passenger attached to you. + - Config Key: `hide-nametag-f5-passengers` + - Values + - Type: `Boolean` + - Default: `false` + - __`REPLACE_OWN_NAMETAG_COLOR`__ - Config Key: `replace-own-nametag-color` - Values diff --git a/docs/developers/mods/overlaymod.mdx b/docs/developers/mods/overlaymod.mdx index 0b692ed4..7eb6fe84 100644 --- a/docs/developers/mods/overlaymod.mdx +++ b/docs/developers/mods/overlaymod.mdx @@ -36,6 +36,14 @@ public void toggleOverlayExample(Player viewer, boolean value) { - Minimum: `0.0F` - Maximum: `2.0F` +- __`FIRE_BLOCK_HEIGHT`__ + - Config Key: `fire-block-height` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.0F` + - Maximum: `2.0F` + - __`SHIELD_HEIGHT`__ - Config Key: `shield-height` - Values diff --git a/docs/developers/mods/ping.mdx b/docs/developers/mods/ping.mdx index 7f3b9672..63f734c2 100644 --- a/docs/developers/mods/ping.mdx +++ b/docs/developers/mods/ping.mdx @@ -22,11 +22,11 @@ public void togglePingExample(Player viewer, boolean value) { - Default: `false` - __`UPDATE_INTERVAL_SEC`__ - - Faster updates may impact performance + - How often ping is measured. If you have trouble connecting to a server, try increasing this. - Config Key: `update-interval-sec` - Values - Type: `Integer` - - Default: `20` + - Default: `5` - Minimum: `1` - Maximum: `120` diff --git a/docs/developers/mods/potioncounter.mdx b/docs/developers/mods/potioncounter.mdx new file mode 100644 index 00000000..0488005d --- /dev/null +++ b/docs/developers/mods/potioncounter.mdx @@ -0,0 +1,118 @@ +# Potion Counter + +Displays how many healing potions or soups are currently in your inventory on the HUD. + +## Integration + +### How to toggle the mod + +```java +public void togglePotionCounterExample(Player viewer, boolean value) { + Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()); + apolloPlayerOpt.ifPresent(apolloPlayer -> this.modSettingModule.getOptions().set(apolloPlayer, ModPotionCounter.ENABLED, value)); +} +``` + +## Available options + +- __`ENABLED`__ + - Config Key: `enabled` + - Values + - Type: `Boolean` + - Default: `false` + +- __`SCALE`__ + - Config Key: `scale` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.25F` + - Maximum: `5.0F` + +- __`TEXT_SHADOW`__ + - Adds a shadow to text + - Config Key: `text-shadow` + - Values + - Type: `Boolean` + - Default: `true` + +- __`BRACKETS`__ + - Config Key: `brackets` + - Values + - Type: `Boolean` + - Default: `true` + +- __`BRACKET_COLOR`__ + - Config Key: `bracket-color` + - Values + - Type: `String` + - Default: `#FFFFFFFF` + +- __`BACKGROUND`__ + - Config Key: `background` + - Values + - Type: `Boolean` + - Default: `true` + +- __`STATIC_BACKGROUND_WIDTH`__ + - If this is disabled the background will change size with the text. + - Config Key: `static-background-width` + - Values + - Type: `Boolean` + - Default: `true` + +- __`STATIC_BACKGROUND_HEIGHT`__ + - If this is disabled the background will change size with the text. + - Config Key: `static-background-height` + - Values + - Type: `Boolean` + - Default: `true` + +- __`BACKGROUND_WIDTH`__ + - Config Key: `background-width` + - Values + - Type: `Integer` + - Default: `56` + - Minimum: `40` + - Maximum: `62` + +- __`BACKGROUND_HEIGHT`__ + - Config Key: `background-height` + - Values + - Type: `Integer` + - Default: `18` + - Minimum: `10` + - Maximum: `22` + +- __`BORDER`__ + - Config Key: `border` + - Values + - Type: `Boolean` + - Default: `false` + +- __`BORDER_THICKNESS`__ + - Config Key: `border-thickness` + - Values + - Type: `Float` + - Default: `0.5F` + - Minimum: `0.5F` + - Maximum: `3.0F` + +- __`BORDER_COLOR`__ + - Config Key: `border-color` + - Values + - Type: `String` + - Default: `#9F000000` + +- __`BACKGROUND_COLOR`__ + - Config Key: `background-color` + - Values + - Type: `String` + - Default: `#6F000000` + +- __`TEXT_COLOR`__ + - Config Key: `text-color` + - Values + - Type: `String` + - Default: `#FFFFFFFF` + diff --git a/docs/developers/mods/potioneffects.mdx b/docs/developers/mods/potioneffects.mdx index be3dca9c..568c098e 100644 --- a/docs/developers/mods/potioneffects.mdx +++ b/docs/developers/mods/potioneffects.mdx @@ -21,76 +21,103 @@ public void togglePotionEffectsExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `true` -- __`SCALE`__ - - Config Key: `scale` +- __`HIDE_AMBIENT_DURATION`__ + - Config Key: `hide-ambient-duration` - Values - - Type: `Float` - - Default: `1.0F` - - Minimum: `0.25F` - - Maximum: `5.0F` + - Type: `Boolean` + - Default: `false` -- __`SHOW_IN_INVENTORY`__ - - Config Key: `show-in-inventory` +- __`VANILLA_GROUP_EFFECTS`__ + - Put the good effects and the bad effects on separate lines + - Config Key: `vanilla-group-effects` - Values - Type: `Boolean` - Default: `true` -- __`SHOW_WHILE_TYPING`__ - - Config Key: `show-while-typing` +- __`VANILLA_MODE_HORIZONTAL`__ + - Config Key: `vanilla-mode-horizontal` - Values - Type: `Boolean` - Default: `true` +- __`VANILLA_MODE_TILES_PER_LINE`__ + - Config Key: `vanilla-mode-tiles-per-line` + - Values + - Type: `Integer` + - Default: `10` + - Minimum: `1` + - Maximum: `10` + +- __`MINIMAL_MODE_HORIZONTAL`__ + - Config Key: `minimal-mode-horizontal` + - Values + - Type: `Boolean` + - Default: `false` + +- __`MINIMAL_MODE_TILES_PER_LINE`__ + - Config Key: `minimal-mode-tiles-per-line` + - Values + - Type: `Integer` + - Default: `4` + - Minimum: `1` + - Maximum: `10` + - __`EFFECT_NAME`__ - Config Key: `effect-name` - Values - Type: `Boolean` - Default: `true` -- __`TEXT_SHADOW`__ - - Adds a shadow to text - - Config Key: `text-shadow` +- __`EFFECT_DURATION`__ + - Config Key: `effect-duration` - Values - Type: `Boolean` - Default: `true` -- __`BACKGROUND`__ - - Config Key: `background` +- __`EFFECT_AMPLIFIER`__ + - Config Key: `effect-amplifier` - Values - Type: `Boolean` - Default: `true` -- __`MINIMAL_MODE`__ - - Config Key: `minimal-mode` +- __`SHOW_IN_INVENTORY`__ + - Config Key: `show-in-inventory` - Values - Type: `Boolean` - - Default: `false` + - Default: `true` -- __`MINIMAL_MODE_HORIZONTAL`__ - - Config Key: `minimal-mode-horizontal` +- __`SHOW_WHILE_TYPING`__ + - Config Key: `show-while-typing` - Values - Type: `Boolean` - - Default: `false` + - Default: `true` -- __`BORDER`__ - - Config Key: `border` +- __`POTION_BLINK`__ + - Config Key: `potion-blink` + - Values + - Type: `Boolean` + - Default: `true` + +- __`POTION_BLINK_ICON`__ + - With this enabled, the potion effect icon blinks as well + - Config Key: `potion-blink-icon` - Values - Type: `Boolean` - Default: `false` -- __`BORDER_THICKNESS`__ - - Config Key: `border-thickness` +- __`BLINK_DURATION`__ + - Config Key: `blink-duration` - Values - - Type: `Float` - - Default: `0.5F` - - Minimum: `0.5F` - - Maximum: `3.0F` + - Type: `Integer` + - Default: `10` + - Minimum: `2` + - Maximum: `20` -- __`FORMATTED_DURATIONS`__ - - Config Key: `formatted-durations` +- __`VANILLA_BLINK_COLOR`__ + - Config Key: `vanilla-blink-color` - Values - - Type: `Boolean` - - Default: `false` + - Type: `String` + - Default: `#FFFFFFFF` - __`UPPERCASE_POTION_NAMES`__ - Config Key: `uppercase-potion-names` @@ -104,25 +131,30 @@ public void togglePotionEffectsExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `false` +- __`FORMATTED_DURATIONS`__ + - Config Key: `formatted-durations` + - Values + - Type: `Boolean` + - Default: `false` + - __`HIDE_MODERN_ICONS`__ - Config Key: `hide-modern-icons` - Values - Type: `Boolean` - Default: `true` -- __`POTION_BLINK`__ - - Config Key: `potion-blink` +- __`HIDE_POTION_STATUS`__ + - Completely hide the potion effects mod HUD + - Config Key: `hide-potion-status` - Values - Type: `Boolean` - - Default: `true` + - Default: `false` -- __`BLINK_DURATION`__ - - Config Key: `blink-duration` +- __`BACKGROUND`__ + - Config Key: `background` - Values - - Type: `Integer` - - Default: `10` - - Minimum: `2` - - Maximum: `20` + - Type: `Boolean` + - Default: `true` - __`BACKGROUND_COLOR`__ - Config Key: `background-color` @@ -130,30 +162,87 @@ public void togglePotionEffectsExample(Player viewer, boolean value) { - Type: `String` - Default: `#6F000000` +- __`BORDER`__ + - Config Key: `border` + - Values + - Type: `Boolean` + - Default: `false` + +- __`BORDER_THICKNESS`__ + - Config Key: `border-thickness` + - Values + - Type: `Float` + - Default: `0.5F` + - Minimum: `0.5F` + - Maximum: `3.0F` + - __`BORDER_COLOR`__ - Config Key: `border-color` - Values - Type: `String` - Default: `#9F000000` +- __`TEXT_SHADOW`__ + - Adds a shadow to text + - Config Key: `text-shadow` + - Values + - Type: `Boolean` + - Default: `true` + - __`COLOR_NAME_BASED_ON_EFFECT`__ - Config Key: `color-name-based-on-effect` - Values - Type: `Boolean` - Default: `false` +- __`COLOR_INFO_BASED_ON_EFFECT`__ + - Config Key: `color-info-based-on-effect` + - Values + - Type: `Boolean` + - Default: `false` + +- __`SHOW_EFFECT_BACKGROUND`__ + - Config Key: `show-effect-background` + - Values + - Type: `Boolean` + - Default: `true` + - __`TEXT_COLOR`__ - Config Key: `text-color` - Values - Type: `String` - Default: `#FFFFFFFF` -- __`DURATION_COLOR`__ - - Config Key: `duration-color` +- __`INFO_COLOR`__ + - Config Key: `info-color` - Values - Type: `String` - Default: `#FFFFFFFF` +- __`VANILLA_ICON_SCALE`__ + - Config Key: `vanilla-icon-scale` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.5F` + - Maximum: `1.25F` + +- __`VANILLA_TEXT_SCALE`__ + - Config Key: `vanilla-text-scale` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.25F` + - Maximum: `2.0F` + +- __`SCALE`__ + - Config Key: `scale` + - Values + - Type: `Float` + - Default: `1.0F` + - Minimum: `0.25F` + - Maximum: `5.0F` + - __`EXCLUDE_PERM`__ - Config Key: `exclude-perm` - Values diff --git a/docs/developers/mods/scoreboard.mdx b/docs/developers/mods/scoreboard.mdx index b5672519..76dc26b6 100644 --- a/docs/developers/mods/scoreboard.mdx +++ b/docs/developers/mods/scoreboard.mdx @@ -62,6 +62,12 @@ public void toggleScoreboardExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `false` +- __`DISPLAY_TOGGLE_MESSAGE`__ + - Config Key: `display-toggle-message` + - Values + - Type: `Boolean` + - Default: `true` + - __`BACKGROUND_COLOR`__ - Config Key: `background-color` - Values diff --git a/docs/developers/mods/screenshot.mdx b/docs/developers/mods/screenshot.mdx index 41283ac9..690e575f 100644 --- a/docs/developers/mods/screenshot.mdx +++ b/docs/developers/mods/screenshot.mdx @@ -45,6 +45,12 @@ public void toggleScreenshotUploaderExample(Player viewer, boolean value) { - Type: `Boolean` - Default: `true` +- __`DELETE_OPTION`__ + - Config Key: `delete-option` + - Values + - Type: `Boolean` + - Default: `true` + - __`WORLD_DETAILS`__ - A screenshot file saved with world details enabled has metadata that contains your player coordinates, the biome, and more. - Config Key: `world-details` diff --git a/docs/developers/mods/skyblock.mdx b/docs/developers/mods/skyblock.mdx index 52ed47b8..a241c529 100644 --- a/docs/developers/mods/skyblock.mdx +++ b/docs/developers/mods/skyblock.mdx @@ -29,16 +29,16 @@ public void toggleHypixelSkyblockExample(Player viewer, boolean value) { - Minimum: `0.2F` - Maximum: `2.5F` -- __`SKYBLOCK_AUTOCOMPLETE_WARPS`__ - - Adds tab completion to the /warp command. - - Config Key: `skyblock-autocomplete-warps` +- __`MIDDLE_CLICK_ARMOR_FIX`__ + - Fixes being unable to use the "Pick Block" keybind on items when it is bound to a mouse button. + - Config Key: `middle-click-armor-fix` - Values - Type: `Boolean` - Default: `true` -- __`MIDDLE_CLICK_ARMOR_FIX`__ - - Fixes being unable to use the "Pick Block" keybind on items when it is bound to a mouse button. - - Config Key: `middle-click-armor-fix` +- __`SKYBLOCK_AUTOCOMPLETE_WARPS`__ + - Adds tab completion to the /warp command. + - Config Key: `skyblock-autocomplete-warps` - Values - Type: `Boolean` - Default: `true` @@ -112,7 +112,7 @@ public void toggleHypixelSkyblockExample(Player viewer, boolean value) { - Config Key: `custom-chime-volume` - Values - Type: `Float` - - Default: `5.0F` + - Default: `0.5F` - Minimum: `0.0F` - Maximum: `1.0F` diff --git a/docs/developers/mods/tiertagger.mdx b/docs/developers/mods/tiertagger.mdx index 704c4f8d..08fa2d9a 100644 --- a/docs/developers/mods/tiertagger.mdx +++ b/docs/developers/mods/tiertagger.mdx @@ -1,6 +1,6 @@ # Tier Tagger -Show player's PvP tier on their Name Tag +Show player's PvP tier on their Name Tag. Also has a /lctiers command to look up player tiers ## Integration @@ -63,6 +63,12 @@ public void toggleTierTaggerExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFFFCF4A` +- __`COLOR_MT1`__ + - Config Key: `color-m-t1` + - Values + - Type: `String` + - Default: `#FFEAC14F` + - __`COLOR_LT1`__ - Config Key: `color-l-t1` - Values @@ -75,6 +81,12 @@ public void toggleTierTaggerExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFA4B3C7` +- __`COLOR_MT2`__ + - Config Key: `color-m-t2` + - Values + - Type: `String` + - Default: `#FF96A0AE` + - __`COLOR_LT2`__ - Config Key: `color-l-t2` - Values @@ -87,6 +99,12 @@ public void toggleTierTaggerExample(Player viewer, boolean value) { - Type: `String` - Default: `#FFDD8849` +- __`COLOR_MT3`__ + - Config Key: `color-m-t3` + - Values + - Type: `String` + - Default: `#FFC8783C` + - __`COLOR_LT3`__ - Config Key: `color-l-t3` - Values @@ -99,6 +117,12 @@ public void toggleTierTaggerExample(Player viewer, boolean value) { - Type: `String` - Default: `#FF655B79` +- __`COLOR_MT4`__ + - Config Key: `color-m-t4` + - Values + - Type: `String` + - Default: `#FF655B79` + - __`COLOR_LT4`__ - Config Key: `color-l-t4` - Values @@ -111,6 +135,12 @@ public void toggleTierTaggerExample(Player viewer, boolean value) { - Type: `String` - Default: `#FF655B79` +- __`COLOR_MT5`__ + - Config Key: `color-m-t5` + - Values + - Type: `String` + - Default: `#FF655B79` + - __`COLOR_LT5`__ - Config Key: `color-l-t5` - Values From 9a11430ce0c99725a7251d99b2e851c08e827f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Bu=C4=8Dari=C4=87?= Date: Mon, 17 Aug 2026 15:01:04 +0200 Subject: [PATCH 9/9] Bump to 1.2.9 (#308) --- docs/developers/minestom.mdx | 6 +++--- example/bukkit/api/src/main/resources/plugin.yml | 2 +- example/bukkit/json/src/main/resources/plugin.yml | 2 +- example/bukkit/proto/src/main/resources/plugin.yml | 2 +- gradle.properties | 2 +- platform/bukkit/src/platform-loader/resources/plugin.yml | 2 +- platform/bungee/src/platform-loader/resources/plugin.yml | 2 +- platform/folia/src/main/resources/plugin.yml | 2 +- .../java/com/lunarclient/apollo/ApolloMinestomPlatform.java | 2 +- .../java/com/lunarclient/apollo/ApolloVelocityPlatform.java | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/developers/minestom.mdx b/docs/developers/minestom.mdx index 0cd257ac..996eea83 100644 --- a/docs/developers/minestom.mdx +++ b/docs/developers/minestom.mdx @@ -52,14 +52,14 @@ Next, add the `apollo-minestom` dependency to your project. ```kotlin filename="build.gradle.kts" dependencies { - implementation("com.lunarclient:apollo-minestom:1.2.8") + implementation("com.lunarclient:apollo-minestom:1.2.9") } ``` ```groovy filename="build.gradle" dependencies { - implementation 'com.lunarclient:apollo-minestom:1.2.8' + implementation 'com.lunarclient:apollo-minestom:1.2.9' } ``` @@ -69,7 +69,7 @@ Next, add the `apollo-minestom` dependency to your project. com.lunarclient apollo-minestom - 1.2.8 + 1.2.9 compile diff --git a/example/bukkit/api/src/main/resources/plugin.yml b/example/bukkit/api/src/main/resources/plugin.yml index c16d024b..9ac047c9 100644 --- a/example/bukkit/api/src/main/resources/plugin.yml +++ b/example/bukkit/api/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-API-Example main: com.lunarclient.apollo.example.api.ApolloApiExamplePlatform -version: 1.2.8 +version: 1.2.9 author: Moonsworth softdepend: [ Apollo-Bukkit, Apollo-Folia ] api-version: 1.13 diff --git a/example/bukkit/json/src/main/resources/plugin.yml b/example/bukkit/json/src/main/resources/plugin.yml index 0b4a5910..64cd0c62 100644 --- a/example/bukkit/json/src/main/resources/plugin.yml +++ b/example/bukkit/json/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Json-Example main: com.lunarclient.apollo.example.json.ApolloJsonExamplePlatform -version: 1.2.8 +version: 1.2.9 author: Moonsworth softdepend: [ Apollo-Bukkit ] api-version: 1.13 diff --git a/example/bukkit/proto/src/main/resources/plugin.yml b/example/bukkit/proto/src/main/resources/plugin.yml index bab850d2..da8661d0 100644 --- a/example/bukkit/proto/src/main/resources/plugin.yml +++ b/example/bukkit/proto/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Proto-Example main: com.lunarclient.apollo.example.proto.ApolloProtoExamplePlatform -version: 1.2.8 +version: 1.2.9 author: Moonsworth softdepend: [ Apollo-Bukkit ] api-version: 1.13 diff --git a/gradle.properties b/gradle.properties index 43cc0bbd..5909961d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.lunarclient -version=1.2.9-SNAPSHOT +version=1.2.9 description=The API for interacting with Lunar Client players. org.gradle.parallel=true diff --git a/platform/bukkit/src/platform-loader/resources/plugin.yml b/platform/bukkit/src/platform-loader/resources/plugin.yml index 6be0fb25..914e6589 100644 --- a/platform/bukkit/src/platform-loader/resources/plugin.yml +++ b/platform/bukkit/src/platform-loader/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Bukkit main: com.lunarclient.apollo.loader.BukkitPlatformLoader -version: 1.2.8 +version: 1.2.9 author: Moonsworth api-version: 1.13 soft-depend: [LunarClient-API] diff --git a/platform/bungee/src/platform-loader/resources/plugin.yml b/platform/bungee/src/platform-loader/resources/plugin.yml index 2baf5bca..633e3deb 100644 --- a/platform/bungee/src/platform-loader/resources/plugin.yml +++ b/platform/bungee/src/platform-loader/resources/plugin.yml @@ -1,4 +1,4 @@ name: Apollo-Bungee main: com.lunarclient.apollo.loader.BungeePlatformLoader -version: 1.2.8 +version: 1.2.9 author: Moonsworth diff --git a/platform/folia/src/main/resources/plugin.yml b/platform/folia/src/main/resources/plugin.yml index 25c7425f..3e38f5df 100644 --- a/platform/folia/src/main/resources/plugin.yml +++ b/platform/folia/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: Apollo-Folia main: com.lunarclient.apollo.ApolloFoliaPlatform -version: 1.2.8 +version: 1.2.9 author: Moonsworth api-version: 1.13 folia-supported: true diff --git a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java index c0d1d3cf..60ac7559 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -253,7 +253,7 @@ public Options getOptions() { @Override public String getApolloVersion() { - return "1.2.8"; + return "1.2.9"; } @Override 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 6c9138c1..cacb9393 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -117,7 +117,7 @@ @Plugin( id = "apollo", name = "Apollo-Velocity", - version = "1.2.8", + version = "1.2.9", url = "https://moonsworth.com", description = "Implementation of Apollo for Velocity", authors = {"Moonsworth"}