From 1741aff5f03b2fbf5e29b5d2765918511e5bb1fc Mon Sep 17 00:00:00 2001 From: ItsNature Date: Fri, 24 Jul 2026 05:12:04 +0200 Subject: [PATCH 1/3] Inventory & Chat Buttons --- .../lunarclient/apollo/ApolloPlatform.java | 34 ++ .../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 | 121 +++++ .../module/inventory/InventoryButton.java | 148 +++++ .../module/inventory/InventoryButtonBox.java | 35 ++ .../module/inventory/InventoryModule.java | 118 +++- .../module/inventory/InventoryType.java | 34 ++ .../PacketEnrichmentModule.java | 46 ++ .../module/button/ButtonModuleSupport.java | 504 ++++++++++++++++++ .../apollo/module/button/ButtonSurface.java | 105 ++++ .../apollo/module/chat/ChatModuleImpl.java | 147 ++++- .../module/inventory/InventoryModuleImpl.java | 198 +++++++ .../PacketEnrichmentImpl.java | 34 ++ .../apollo/network/ButtonNetworkTypes.java | 262 +++++++++ .../apollo/network/NetworkTypes.java | 8 + docs/developers/events.mdx | 30 ++ .../lightweight/json/serverbound-packets.mdx | 24 + .../protobuf/serverbound-packets.mdx | 28 + docs/developers/modules/packetenrichment.mdx | 28 + docs/developers/utilities/_meta.json | 1 + docs/developers/utilities/icons.mdx | 12 +- .../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 + 86 files changed, 8557 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/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/inventory/InventoryModuleImpl.java create mode 100644 common/src/main/java/com/lunarclient/apollo/network/ButtonNetworkTypes.java 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..c5bbd0f9 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,29 @@ 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. + * + *

Repeating tasks die with the platform plugin, so they need no + * explicit cancellation handle.

+ * + * @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..1325d265 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,18 @@ */ package com.lunarclient.apollo.module.chat; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; import com.lunarclient.apollo.module.ApolloModule; import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.option.Option; +import com.lunarclient.apollo.option.SimpleOption; import com.lunarclient.apollo.recipients.Recipients; +import io.leangen.geantyref.TypeToken; +import java.util.Collection; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; /** * Represents the chat module. @@ -38,6 +45,37 @@ @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("live-buttons", "broadcast").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); + + protected ChatModule() { + this.registerOptions( + ChatModule.BROADCAST_LIVE_BUTTONS + ); + } + + @Override + public boolean isClientNotify() { + return true; + } + /** * Displays the message to the {@link Recipients}. * @@ -57,4 +95,87 @@ 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); + } 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..9560fdbf 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,17 @@ */ package com.lunarclient.apollo.module.inventory; +import com.lunarclient.apollo.common.button.ApolloButtonTooltip; +import com.lunarclient.apollo.common.button.content.ApolloButtonContent; import com.lunarclient.apollo.module.ApolloModule; import com.lunarclient.apollo.module.ModuleDefinition; +import com.lunarclient.apollo.option.Option; +import com.lunarclient.apollo.option.SimpleOption; +import com.lunarclient.apollo.recipients.Recipients; +import io.leangen.geantyref.TypeToken; +import java.util.Collection; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; /** * Represents the inventory module. @@ -34,10 +42,33 @@ */ @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("live-buttons", "broadcast").type(TypeToken.get(Boolean.class)) + .defaultValue(false).build(); protected InventoryModule() { - this.registerOptions(ApolloModule.ENABLE_OPTION_OFF); + this.registerOptions( + ApolloModule.ENABLE_OPTION_OFF, + InventoryModule.BROADCAST_LIVE_BUTTONS + ); } @Override @@ -45,4 +76,87 @@ 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); + } 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/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/ChatModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java index ae4ee548..2e8b4d24 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,62 @@ */ 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.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.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 { + + 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.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 +99,107 @@ 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); + } + + @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/InventoryModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java new file mode 100644 index 00000000..c0e4f4d9 --- /dev/null +++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java @@ -0,0 +1,198 @@ +/* + * 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.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.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 { + + 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.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); + } + + @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 1446ef11..5f785fbd 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; @@ -106,6 +110,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/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/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/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/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..04c86422 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 @@ -41,6 +41,7 @@ import com.lunarclient.apollo.example.api.module.EntityApiExample; import com.lunarclient.apollo.example.api.module.GlowApiExample; 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; @@ -87,6 +88,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..45366a7b --- /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.live-buttons.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..ff8fe1d2 --- /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.live-buttons.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..0c15fb05 --- /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.live-buttons.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 c39d80f2..1415b4e1 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 @@ -199,7 +199,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 37e62f7c..b789dea2 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 391c46d7..64fcb7e5 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 @@ -37,6 +37,7 @@ import com.lunarclient.apollo.example.json.module.EntityJsonExample; import com.lunarclient.apollo.example.json.module.GlowJsonExample; 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; @@ -75,6 +76,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 94578ad4..664da099 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 @@ -37,6 +37,7 @@ import com.lunarclient.apollo.example.proto.module.EntityProtoExample; import com.lunarclient.apollo.example.proto.module.GlowProtoExample; 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; @@ -75,6 +76,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 d83b4d64..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.0" +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 6de2e988..93ce25ed 100644 --- a/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java +++ b/platform/bukkit/src/main/java/com/lunarclient/apollo/ApolloBukkitPlatform.java @@ -53,6 +53,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,6 +99,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; @@ -150,7 +152,7 @@ public void onEnable() { .addModule(GlintModule.class) .addModule(GlowModule.class, new GlowModuleImpl()) .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()) @@ -224,6 +226,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 a5f50d27..777d30ac 100644 --- a/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java +++ b/platform/bungee/src/main/java/com/lunarclient/apollo/ApolloBungeePlatform.java @@ -48,6 +48,8 @@ import com.lunarclient.apollo.module.entity.EntityModuleImpl; 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; @@ -86,6 +88,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; @@ -132,6 +135,7 @@ public void onEnable() { .addModule(CooldownModule.class, new CooldownModuleImpl()) .addModule(EntityModule.class, new EntityModuleImpl()) .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()) @@ -202,4 +206,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 19a4878b..06e2ff44 100644 --- a/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java +++ b/platform/folia/src/main/java/com/lunarclient/apollo/ApolloFoliaPlatform.java @@ -50,6 +50,8 @@ import com.lunarclient.apollo.module.glow.GlowModuleImpl; 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; @@ -94,6 +96,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; @@ -139,6 +142,7 @@ public void onEnable() { .addModule(EntityModule.class, new EntityModuleImpl()) .addModule(GlowModule.class, new GlowModuleImpl()) .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()) @@ -207,6 +211,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 8c2e26ab..63e48c08 100644 --- a/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java +++ b/platform/minestom/src/main/java/com/lunarclient/apollo/ApolloMinestomPlatform.java @@ -52,6 +52,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; @@ -96,9 +97,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; @@ -172,7 +176,7 @@ public static void init(ApolloMinestomProperties properties) { .addModule(GlintModule.class) .addModule(GlowModule.class, new GlowModuleImpl()) .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()) @@ -254,6 +258,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 8d3ed35f..e5661538 100644 --- a/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java +++ b/platform/velocity/src/main/java/com/lunarclient/apollo/ApolloVelocityPlatform.java @@ -48,6 +48,8 @@ import com.lunarclient.apollo.module.entity.EntityModuleImpl; 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 +102,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; @@ -169,6 +172,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(); @@ -199,6 +218,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { .addModule(CooldownModule.class, new CooldownModuleImpl()) .addModule(EntityModule.class, new EntityModuleImpl()) .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 1e5e82c6bb83f230dd0b98d50bcd4487e5f13292 Mon Sep 17 00:00:00 2001 From: ItsNature Date: Tue, 28 Jul 2026 05:41:45 +0200 Subject: [PATCH 2/3] Add default chat & inventory buttons config option --- .github/workflows/deploy.yml | 2 +- .../lunarclient/apollo/ApolloPlatform.java | 3 - .../apollo/module/chat/ChatModule.java | 96 +++++- .../module/inventory/InventoryModule.java | 217 +++++++++++++- .../module/button/ApolloButtonSerializer.java | 273 ++++++++++++++++++ .../module/chat/ChatButtonSerializer.java | 57 ++++ .../apollo/module/chat/ChatModuleImpl.java | 25 +- .../inventory/InventoryButtonSerializer.java | 66 +++++ .../module/inventory/InventoryModuleImpl.java | 25 +- .../option/config/CommonSerializers.java | 208 +++++++++++++ .../module/inventorybuttons/MenuLayout.java | 2 +- .../inventorybuttons/MinigameLayout.java | 2 +- .../module/inventorybuttons/StaffLayout.java | 2 +- .../apollo/common/ApolloComponent.java | 24 ++ 14 files changed, 981 insertions(+), 21 deletions(-) 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/chat/ChatButtonSerializer.java create mode 100644 common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c3e26d2..a7e613ec 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: fi - name: Gradle Publish - if: "${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/version/') }}" + if: "${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/version/') || github.ref == 'refs/heads/feature/inventory-and-chat-buttons' }}" run: ./gradlew publish - name: Gradle Release diff --git a/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java b/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java index c5bbd0f9..4679d873 100644 --- a/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java +++ b/api/src/main/java/com/lunarclient/apollo/ApolloPlatform.java @@ -136,9 +136,6 @@ interface Scheduler { * Schedules a task to run asynchronously every period, first running * after the given delay. * - *

Repeating tasks die with the platform plugin, so they need no - * explicit cancellation handle.

- * * @param task the task to run * @param delay the delay before the first run * @param period the period between runs 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 1325d265..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,16 +23,26 @@ */ 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; @@ -58,16 +68,47 @@ public abstract class ChatModule extends ApolloModule { * @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("live-buttons", "broadcast").type(TypeToken.get(Boolean.class)) + .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.BROADCAST_LIVE_BUTTONS, + ChatModule.SEND_DEFAULT_BUTTONS, + ChatModule.DEFAULT_BUTTONS ); } @@ -178,4 +219,47 @@ public abstract void updateChatButtonContent(Recipients recipients, String butto 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/InventoryModule.java b/api/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModule.java index 9560fdbf..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,15 +23,26 @@ */ 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; @@ -57,17 +68,48 @@ public abstract class InventoryModule extends ApolloModule { * @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("live-buttons", "broadcast").type(TypeToken.get(Boolean.class)) + .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, - InventoryModule.BROADCAST_LIVE_BUTTONS + InventoryModule.BROADCAST_LIVE_BUTTONS, + InventoryModule.SEND_DEFAULT_BUTTONS, + InventoryModule.DEFAULT_BUTTONS ); } @@ -159,4 +201,167 @@ public abstract void updateInventoryButtonContent(Recipients recipients, String 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/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/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 2e8b4d24..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 @@ -38,12 +38,14 @@ 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; @@ -58,7 +60,7 @@ * * @since 1.0.2 */ -public final class ChatModuleImpl extends ChatModule implements ButtonSurface { +public final class ChatModuleImpl extends ChatModule implements ButtonSurface, Serializer { private final ButtonModuleSupport support = new ButtonModuleSupport<>(this, this, ChatModule.BROADCAST_LIVE_BUTTONS); @@ -70,6 +72,8 @@ public final class ChatModuleImpl extends ChatModule implements ButtonSurface 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())); @@ -144,6 +148,25 @@ public void updateChatButtonTooltip(@NonNull Recipients recipients, @NonNull Str 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); 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 index c0e4f4d9..87590b87 100644 --- a/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java +++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java @@ -30,6 +30,7 @@ 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; @@ -40,6 +41,7 @@ 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; @@ -53,7 +55,7 @@ * * @since 1.2.9 */ -public final class InventoryModuleImpl extends InventoryModule implements ButtonSurface { +public final class InventoryModuleImpl extends InventoryModule implements ButtonSurface, Serializer { private final ButtonModuleSupport support = new ButtonModuleSupport<>(this, this, InventoryModule.BROADCAST_LIVE_BUTTONS); @@ -65,6 +67,8 @@ public final class InventoryModuleImpl extends InventoryModule implements Button */ 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())); @@ -130,6 +134,25 @@ public void updateInventoryButtonTooltip(@NonNull Recipients recipients, @NonNul 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); 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/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 index 45366a7b..8bb5dace 100644 --- 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 @@ -48,7 +48,7 @@ public final class MenuLayout { public static void display(InventoryModule inventoryModule, Player viewer) { // Live parts refresh automatically only while the Apollo config enables - // modules.inventory.live-buttons.broadcast (see the inventory module docs) + // modules.inventory.buttons.live-broadcast (see the inventory module docs) Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { InventoryButton shop = InventoryButton.builder() .id("shop") 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 index ff8fe1d2..dc1633db 100644 --- 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 @@ -54,7 +54,7 @@ public final class MinigameLayout { public static void display(InventoryModule inventoryModule, Player viewer) { // Live parts refresh automatically only while the Apollo config enables - // modules.inventory.live-buttons.broadcast (see the inventory module docs) + // modules.inventory.buttons.live-broadcast (see the inventory module docs) Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { InventoryButton mapInfo = InventoryButton.builder() .id("map-info") 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 index 0c15fb05..71922aca 100644 --- 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 @@ -52,7 +52,7 @@ public final class StaffLayout { public static void display(InventoryModule inventoryModule, Player viewer) { // Live parts refresh automatically only while the Apollo config enables - // modules.inventory.live-buttons.broadcast (see the inventory module docs) + // modules.inventory.buttons.live-broadcast (see the inventory module docs) Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> { InventoryButton players = InventoryButton.builder() .id("players") 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..230ba6e5 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 the component parsed from a legacy {@link String} with + * ampersand ({@code &}) color codes. + * + * @param text the legacy string to make into a component + * @return the component for this legacy string + * @since 1.2.9 + */ + public static Component fromLegacyAmpersand(@NonNull String text) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(text); + } + + /** + * Returns this component as a legacy {@link String} with ampersand + * ({@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() { } From 63352f1a7a84ead16053557c8d5bdb66cd63deef Mon Sep 17 00:00:00 2001 From: ItsNature Date: Tue, 28 Jul 2026 07:22:28 +0200 Subject: [PATCH 3/3] Add button docs --- docs/developers/modules/chat.mdx | 666 ++++++++++++ docs/developers/modules/inventory.mdx | 1431 +++++++++++++++++++++++++ docs/developers/utilities/buttons.mdx | 306 ++++++ 3 files changed, 2403 insertions(+) create mode 100644 docs/developers/utilities/buttons.mdx diff --git a/docs/developers/modules/chat.mdx b/docs/developers/modules/chat.mdx index 6065ed97..8c0c3daa 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,665 @@ 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 re-resolved and +re-sent automatically 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. + +# TODO: Add image/gif + +```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. + +# TODO: Add image/gif + +```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 35615fe2..c9f46d91 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,1430 @@ 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 re-resolved and +re-sent automatically 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. + +# TODO: Add image/gif + +```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. + +# TODO: Add image/gif + +```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. + +# TODO: Add image/gif + +```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. + +# TODO: Add image/gif + +```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/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.