type, @Nullable String value, String field) throws SerializationException {
+ if (value == null) {
+ throw new SerializationException("Required field " + field + " not found!");
+ }
+
+ try {
+ return Enum.valueOf(type, value.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException exception) {
+ throw new SerializationException("Unknown " + field + " '" + value + "'!");
+ }
+ }
+
+ /**
+ * Returns the node at the given path, requiring it to exist.
+ *
+ * @param source the source node
+ * @param path the child path
+ * @return the child node
+ * @throws SerializationException if the child does not exist
+ * @since 1.2.9
+ */
+ protected ConfigurationNode virtualNode(ConfigurationNode source, Object... path) throws SerializationException {
+ if (!source.hasChild(path)) {
+ throw new SerializationException("Required field " + Arrays.toString(path) + " not found!");
+ }
+
+ return source.node(path);
+ }
+
+ private ApolloButtonAction readAction(ConfigurationNode node) throws SerializationException {
+ if (node.hasChild("run-command")) {
+ return ApolloButtonAction.runCommand(node.node("run-command").getString(""));
+ }
+
+ if (node.hasChild("open-url")) {
+ return ApolloButtonAction.openUrl(node.node("open-url").getString(""));
+ }
+
+ if (node.hasChild("client-action")) {
+ return ApolloButtonAction.clientAction(this.parseEnum(ApolloButtonClientAction.class,
+ node.node("client-action").getString(), "client-action"));
+ }
+
+ throw new SerializationException("on-click requires a 'run-command', 'open-url' or 'client-action' field!");
+ }
+
+ private void writePart(ConfigurationNode node, ApolloButtonContentPart part) throws SerializationException {
+ if (part instanceof LiveComponentPart) {
+ throw new SerializationException("Live content parts cannot be stored in the config!");
+ }
+
+ if (part instanceof ComponentPart) {
+ node.node("text").set(ApolloComponent.toLegacyAmpersand(((ComponentPart) part).getComponent()));
+ return;
+ }
+
+ if (part instanceof IconPart) {
+ node.node("icon").set(Icon.class, ((IconPart) part).getIcon());
+ return;
+ }
+
+ throw new SerializationException("Unknown button content part type: " + part.getClass().getName());
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java
new file mode 100644
index 00000000..be3fada2
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonModuleSupport.java
@@ -0,0 +1,504 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.module.button;
+
+import com.google.protobuf.Message;
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.ApolloManager;
+import com.lunarclient.apollo.button.v1.ButtonUpdate;
+import com.lunarclient.apollo.common.button.ApolloButton;
+import com.lunarclient.apollo.common.button.ApolloButtonSize;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.action.ApolloButtonAction;
+import com.lunarclient.apollo.common.button.action.RunCommandAction;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart;
+import com.lunarclient.apollo.common.button.content.LiveComponentPart;
+import com.lunarclient.apollo.common.location.HudPosition;
+import com.lunarclient.apollo.module.ApolloModule;
+import com.lunarclient.apollo.network.ButtonNetworkTypes;
+import com.lunarclient.apollo.option.SimpleOption;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import com.lunarclient.apollo.recipients.Recipients;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The shared engine behind every button module.
+ *
+ * @param the button type
+ * @param the protobuf message
+ * @since 1.2.9
+ */
+@RequiredArgsConstructor
+public final class ButtonModuleSupport {
+
+ private static final long TICK_MILLIS = 50L;
+
+ private final Set openViewers = ConcurrentHashMap.newKeySet();
+ private final Map> liveButtons = new ConcurrentHashMap<>();
+ private final AtomicBoolean broadcastStarted = new AtomicBoolean(false);
+ private final AtomicBoolean ticking = new AtomicBoolean(false);
+
+ private final ApolloModule owner;
+ private final ButtonSurface surface;
+ private final SimpleOption broadcastOption;
+
+ /**
+ * Starts the repeating live-button broadcast task.
+ *
+ * @since 1.2.9
+ */
+ public void startBroadcast() {
+ if (!this.broadcastStarted.compareAndSet(false, true)) {
+ return;
+ }
+
+ try {
+ Apollo.getPlatform().getScheduler()
+ .scheduleAsyncRepeating(this::broadcastTick, TICK_MILLIS, TICK_MILLIS, TimeUnit.MILLISECONDS);
+ } catch (Throwable throwable) {
+ this.broadcastStarted.set(false);
+ }
+ }
+
+ /**
+ * Validates and displays the buttons, resolving live values per viewer
+ * and registering live buttons for the periodic broadcast.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param buttons the buttons to display
+ * @since 1.2.9
+ */
+ public void displayButtons(Recipients recipients, Collection buttons) {
+ if (buttons.isEmpty()) {
+ throw new IllegalArgumentException("Button collection must not be empty");
+ }
+
+ Set ids = new HashSet<>(buttons.size());
+ List live = new ArrayList<>();
+ for (B button : buttons) {
+ this.surface.validate(button);
+
+ if (!ids.add(button.getId())) {
+ throw new IllegalArgumentException("Duplicate button id '" + button.getId() + "' in display batch");
+ }
+
+ if (this.isLiveButton(button)) {
+ live.add(button);
+ }
+ }
+
+ if (live.isEmpty()) {
+ List elements = new ArrayList<>();
+ for (B button : buttons) {
+ elements.add(this.surface.toDisplayElement(button, null));
+ }
+
+ ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createDisplay(elements));
+ this.untrackButtons(recipients, buttons);
+ return;
+ }
+
+ recipients.forEach(recipient -> {
+ ApolloPlayer player = (ApolloPlayer) recipient;
+
+ List
elements = new ArrayList<>();
+ for (B button : buttons) {
+ elements.add(this.surface.toDisplayElement(button, player));
+ }
+
+ ApolloManager.getNetworkManager().sendPacket(player, this.surface.createDisplay(elements));
+
+ long now = System.currentTimeMillis();
+ Map tracked = this.liveButtons.computeIfAbsent(player.getUniqueId(), uuid -> new ConcurrentHashMap<>());
+
+ for (B button : buttons) {
+ if (this.isLiveButton(button)) {
+ tracked.put(button.getId(), this.createLiveButton(button.getContent(), button.getTooltip(), now));
+ } else {
+ tracked.remove(button.getId());
+ }
+ }
+ });
+ }
+
+ private void untrackButtons(Recipients recipients, Collection buttons) {
+ recipients.forEach(recipient -> {
+ Map tracked = this.liveButtons.get(((ApolloPlayer) recipient).getUniqueId());
+ if (tracked == null) {
+ return;
+ }
+
+ for (B button : buttons) {
+ tracked.remove(button.getId());
+ }
+ });
+ }
+
+ /**
+ * Removes a button by id and drops it from live tracking.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param buttonId the button id
+ * @since 1.2.9
+ */
+ public void removeButton(Recipients recipients, String buttonId) {
+ ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createRemove(buttonId));
+
+ recipients.forEach(recipient -> {
+ Map tracked = this.liveButtons.get(((ApolloPlayer) recipient).getUniqueId());
+ if (tracked != null) {
+ tracked.remove(buttonId);
+ }
+ });
+ }
+
+ /**
+ * Resets all buttons and drops the recipients live tracking.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @since 1.2.9
+ */
+ public void resetButtons(Recipients recipients) {
+ ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createReset());
+ recipients.forEach(recipient -> this.liveButtons.remove(((ApolloPlayer) recipient).getUniqueId()));
+ }
+
+ /**
+ * Pushes an update for a displayed button.
+ *
+ * @param recipients the recipients that are receiving the packet
+ * @param buttonId the button id
+ * @param content the new content, or {@code null} to keep the current one
+ * @param updateTooltip whether the tooltip should be replaced at all
+ * @param tooltip the new tooltip, or {@code null} to clear it (when
+ * {@code updateTooltip} is {@code true})
+ * @since 1.2.9
+ */
+ public void pushUpdate(Recipients recipients, String buttonId,
+ @Nullable ApolloButtonContent content, boolean updateTooltip,
+ @Nullable ApolloButtonTooltip tooltip) {
+ if (buttonId.isEmpty()) {
+ throw new IllegalArgumentException("ApolloButton#id must not be empty");
+ }
+
+ boolean live = (content != null && content.isLive()) || (updateTooltip && tooltip != null && tooltip.isLive());
+
+ if (!live) {
+ ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(content, tooltip, updateTooltip, null);
+ ApolloManager.getNetworkManager().sendPacket(recipients, this.surface.createUpdate(buttonId, update));
+ } else {
+ recipients.forEach(recipient -> {
+ ApolloPlayer player = (ApolloPlayer) recipient;
+
+ ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(content, tooltip, updateTooltip, player);
+ ApolloManager.getNetworkManager().sendPacket(player, this.surface.createUpdate(buttonId, update));
+ });
+ }
+
+ recipients.forEach(recipient -> {
+ Map tracked = this.liveButtons
+ .computeIfAbsent(((ApolloPlayer) recipient).getUniqueId(), uuid -> new ConcurrentHashMap<>());
+
+ long now = System.currentTimeMillis();
+ tracked.compute(buttonId, (id, previous) -> {
+ ApolloButtonContent newContent = content != null ? content
+ : previous != null ? previous.content : null;
+ ApolloButtonTooltip newTooltip = updateTooltip ? tooltip
+ : previous != null ? previous.tooltip : null;
+
+ if (newContent == null || (!newContent.isLive() && (newTooltip == null || !newTooltip.isLive()))) {
+ return null;
+ }
+
+ return this.createLiveButton(newContent, newTooltip, now);
+ });
+ });
+ }
+
+ /**
+ * Marks a player's surface as open and pushes an immediate live refresh
+ * for their tracked buttons; called from the surface's open event.
+ *
+ * @param player the player whose surface opened
+ * @since 1.2.9
+ */
+ public void handleOpen(ApolloPlayer player) {
+ if (!this.owner.isEnabled()) {
+ return;
+ }
+
+ this.openViewers.add(player.getUniqueId());
+
+ if (!this.owner.getOptions().get(this.broadcastOption)) {
+ return;
+ }
+
+ Map tracked = this.liveButtons.get(player.getUniqueId());
+ if (tracked != null && !tracked.isEmpty()) {
+ this.sendLiveUpdates(player, tracked, System.currentTimeMillis(), true);
+ }
+ }
+
+ /**
+ * Marks a player's surface as closed; called from the surface's close
+ * event.
+ *
+ * @param playerIdentifier the player whose surface closed
+ * @since 1.2.9
+ */
+ public void handleClose(UUID playerIdentifier) {
+ this.openViewers.remove(playerIdentifier);
+ }
+
+ /**
+ * Drops all state for a disconnecting player.
+ *
+ * @param playerIdentifier the unregistering player
+ * @since 1.2.9
+ */
+ public void handleUnregister(UUID playerIdentifier) {
+ this.openViewers.remove(playerIdentifier);
+ this.liveButtons.remove(playerIdentifier);
+ }
+
+ private void broadcastTick() {
+ if (!this.ticking.compareAndSet(false, true)) {
+ return;
+ }
+
+ try {
+ if (!this.owner.isEnabled()) {
+ this.liveButtons.clear();
+ this.openViewers.clear();
+ return;
+ }
+
+ if (!this.owner.getOptions().get(this.broadcastOption) || this.liveButtons.isEmpty()) {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ boolean trackingActive = this.surface.isOpenTrackingActive();
+
+ Iterator>> iterator = this.liveButtons.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry> entry = iterator.next();
+ if (entry.getValue().isEmpty()) {
+ iterator.remove();
+ continue;
+ }
+
+ if (trackingActive && !this.openViewers.contains(entry.getKey())) {
+ continue;
+ }
+
+ if (!this.anyUpdateDue(entry.getValue(), now)) {
+ continue;
+ }
+
+ ApolloPlayer player = Apollo.getPlayerManager().getPlayer(entry.getKey()).orElse(null);
+ if (player == null) {
+ iterator.remove();
+ continue;
+ }
+
+ try {
+ this.sendLiveUpdates(player, entry.getValue(), now, false);
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ }
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ } finally {
+ this.ticking.set(false);
+ }
+ }
+
+ private long millisToTicks(Duration interval) {
+ return Math.max(1L, interval.toMillis() / TICK_MILLIS) * TICK_MILLIS;
+ }
+
+ private LiveButton createLiveButton(ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip, long now) {
+ long contentInterval = 0L;
+ for (ApolloButtonContentPart part : content.getParts()) {
+ if (!(part instanceof LiveComponentPart)) {
+ continue;
+ }
+
+ LiveComponentPart livePart = (LiveComponentPart) part;
+ long interval = this.millisToTicks(livePart.getUpdateInterval());
+
+ if (contentInterval == 0L || interval < contentInterval) {
+ contentInterval = interval;
+ }
+ }
+
+ Duration tooltipUpdateInterval = tooltip != null ? tooltip.getUpdateInterval() : null;
+ long tooltipInterval = tooltipUpdateInterval != null ? this.millisToTicks(tooltipUpdateInterval) : 0L;
+
+ return new LiveButton(content, tooltip, contentInterval, tooltipInterval, now);
+ }
+
+ private boolean anyUpdateDue(Map buttons, long now) {
+ for (LiveButton button : buttons.values()) {
+ if (button.isContentDue(now) || button.isTooltipDue(now)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void sendLiveUpdates(ApolloPlayer player, Map buttons, long now, boolean force) {
+ for (Map.Entry entry : buttons.entrySet()) {
+ LiveButton button = entry.getValue();
+
+ boolean contentDue = force ? button.contentIntervalMillis > 0L : button.isContentDue(now);
+ boolean tooltipDue = force ? button.tooltipIntervalMillis > 0L : button.isTooltipDue(now);
+ if (!contentDue && !tooltipDue) {
+ continue;
+ }
+
+ ButtonUpdate update = ButtonNetworkTypes.toUpdateProtobuf(contentDue
+ ? button.content : null, button.tooltip, tooltipDue, player);
+
+ ApolloManager.getNetworkManager().sendPacket(player, this.surface.createUpdate(entry.getKey(), update));
+
+ if (contentDue) {
+ button.nextContentUpdate = now + button.contentIntervalMillis;
+ }
+
+ if (tooltipDue) {
+ button.nextTooltipUpdate = now + button.tooltipIntervalMillis;
+ }
+ }
+ }
+
+ private boolean isLiveButton(ApolloButton button) {
+ ApolloButtonTooltip tooltip = button.getTooltip();
+ return button.getContent().isLive() || (tooltip != null && tooltip.isLive());
+ }
+
+ private static final class LiveButton {
+
+ private final ApolloButtonContent content;
+ private final @Nullable ApolloButtonTooltip tooltip;
+ private final long contentIntervalMillis;
+ private final long tooltipIntervalMillis;
+ private volatile long nextContentUpdate;
+ private volatile long nextTooltipUpdate;
+
+ private LiveButton(ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip,
+ long contentIntervalMillis, long tooltipIntervalMillis, long now) {
+ this.content = content;
+ this.tooltip = tooltip;
+ this.contentIntervalMillis = contentIntervalMillis;
+ this.tooltipIntervalMillis = tooltipIntervalMillis;
+ this.nextContentUpdate = now + contentIntervalMillis;
+ this.nextTooltipUpdate = now + tooltipIntervalMillis;
+ }
+
+ private boolean isContentDue(long now) {
+ return this.contentIntervalMillis > 0L && now >= this.nextContentUpdate;
+ }
+
+ private boolean isTooltipDue(long now) {
+ return this.tooltipIntervalMillis > 0L && now >= this.nextTooltipUpdate;
+ }
+ }
+
+ /**
+ * Validates the button properties.
+ *
+ * @param button the button to validate
+ * @param boxWidth the surface container width
+ * @param boxHeight the surface container height
+ * @since 1.2.9
+ */
+ public static void validateCommon(ApolloButton button, float boxWidth, float boxHeight) {
+ requireSet(button.getId(), "ApolloButton#id");
+ requireSet(button.getPosition(), "ApolloButton#position");
+ requireSet(button.getSize(), "ApolloButton#size");
+ requireSet(button.getShape(), "ApolloButton#shape");
+ requireSet(button.getBackgroundColor(), "ApolloButton#backgroundColor");
+ requireSet(button.getBorderColor(), "ApolloButton#borderColor");
+ requireSet(button.getContent(), "ApolloButton#content");
+
+ if (button.getId().isEmpty()) {
+ throw new IllegalArgumentException("ApolloButton#id must not be empty");
+ }
+
+ HudPosition position = button.getPosition();
+ ApolloButtonSize size = button.getSize();
+
+ if (!Float.isFinite(position.getX()) || !Float.isFinite(position.getY())
+ || !Float.isFinite(size.getWidth()) || !Float.isFinite(size.getHeight())) {
+ throw new IllegalArgumentException("ApolloButton position and size must be finite");
+ }
+
+ if (size.getWidth() <= 0.0F || size.getHeight() <= 0.0F) {
+ throw new IllegalArgumentException("ApolloButton#size width and height must be greater than 0");
+ }
+
+ if (position.getX() < 0.0F || position.getY() < 0.0F
+ || position.getX() + size.getWidth() > boxWidth
+ || position.getY() + size.getHeight() > boxHeight) {
+ throw new IllegalArgumentException("ApolloButton must fit within the " + boxWidth + "x" + boxHeight + " button box");
+ }
+
+ ApolloButtonAction onClick = button.getOnClick();
+ if (onClick instanceof RunCommandAction && !((RunCommandAction) onClick).getCommand().startsWith("/")) {
+ throw new IllegalArgumentException("RunCommandAction#command must start with '/'");
+ }
+ }
+
+ /**
+ * Throws a {@link IllegalArgumentException} when the value is {@code null}.
+ *
+ * @param value the value to check
+ * @param name the field name
+ * @since 1.2.9
+ */
+ public static void requireSet(@Nullable Object value, String name) {
+ if (value == null) {
+ throw new IllegalArgumentException(name + " must not be null");
+ }
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java
new file mode 100644
index 00000000..75fa9701
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/button/ButtonSurface.java
@@ -0,0 +1,105 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.module.button;
+
+import com.google.protobuf.Message;
+import com.lunarclient.apollo.button.v1.ButtonUpdate;
+import com.lunarclient.apollo.common.button.ApolloButton;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The per-surface strategy behind {@link ButtonModuleSupport}.
+ *
+ * @param the button type
+ * @param the protobuf message
+ * @since 1.2.9
+ */
+public interface ButtonSurface {
+
+ /**
+ * Validates a button before it is sent, throwing {@link IllegalArgumentException} on invalid buttons.
+ *
+ * @param button the button to validate
+ * @since 1.2.9
+ */
+ void validate(B button);
+
+ /**
+ * Builds the surface wrapper element for a button, embedding the shared
+ * {@code button.v1.Button} payload alongside the surface placement fields.
+ *
+ * @param button the button to serialize
+ * @param viewer the viewing player live values are resolved for, or {@code null}
+ * @return the wrapper element
+ * @since 1.2.9
+ */
+ P toDisplayElement(B button, @Nullable ApolloPlayer viewer);
+
+ /**
+ * Builds the surface display message from wrapper elements.
+ *
+ * @param elements the wrapper elements
+ * @return the display message
+ * @since 1.2.9
+ */
+ Message createDisplay(List
elements);
+
+ /**
+ * Builds the surface update message for a button id.
+ *
+ * @param buttonId the button id
+ * @param update the shared update fragment
+ * @return the update message
+ * @since 1.2.9
+ */
+ Message createUpdate(String buttonId, ButtonUpdate update);
+
+ /**
+ * Builds the surface remove message for a button id.
+ *
+ * @param buttonId the button id
+ * @return the remove message
+ * @since 1.2.9
+ */
+ Message createRemove(String buttonId);
+
+ /**
+ * Builds the surface reset message.
+ *
+ * @return the reset message
+ * @since 1.2.9
+ */
+ Message createReset();
+
+ /**
+ * Returns whether per-player surface open/close tracking is active.
+ *
+ * @return whether open tracking is active
+ * @since 1.2.9
+ */
+ boolean isOpenTrackingActive();
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java
new file mode 100644
index 00000000..e20999e1
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatButtonSerializer.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.module.chat;
+
+import com.lunarclient.apollo.common.button.ApolloButton;
+import com.lunarclient.apollo.module.button.ApolloButtonSerializer;
+import java.awt.Color;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+/**
+ * Reads and writes {@link ChatButton}s from the module configuration; see
+ * {@link ApolloButtonSerializer} for the shared button fields.
+ *
+ * @since 1.2.9
+ */
+public final class ChatButtonSerializer extends ApolloButtonSerializer {
+
+ @Override
+ protected ApolloButton.ApolloButtonBuilder extends ChatButton, ?> createBuilder(ConfigurationNode node) throws SerializationException {
+ ChatButton.ChatButtonBuilder, ?> builder = ChatButton.builder();
+
+ Color background = node.node("background-color").get(Color.class);
+ if (background != null) {
+ builder.backgroundColor(background);
+ }
+
+ Color border = node.node("border-color").get(Color.class);
+ if (border != null) {
+ builder.borderColor(border);
+ }
+
+ return builder;
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java
index ae4ee548..7b1a7eec 100644
--- a/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java
+++ b/common/src/main/java/com/lunarclient/apollo/module/chat/ChatModuleImpl.java
@@ -23,20 +23,66 @@
*/
package com.lunarclient.apollo.module.chat;
+import com.google.protobuf.Message;
+import com.lunarclient.apollo.Apollo;
import com.lunarclient.apollo.ApolloManager;
+import com.lunarclient.apollo.button.v1.ButtonUpdate;
+import com.lunarclient.apollo.chat.v1.DisplayChatButtonsMessage;
import com.lunarclient.apollo.chat.v1.DisplayLiveChatMessageMessage;
+import com.lunarclient.apollo.chat.v1.RemoveChatButtonMessage;
import com.lunarclient.apollo.chat.v1.RemoveLiveChatMessageMessage;
+import com.lunarclient.apollo.chat.v1.ResetChatButtonsMessage;
+import com.lunarclient.apollo.chat.v1.UpdateChatButtonMessage;
import com.lunarclient.apollo.common.ApolloComponent;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatCloseEvent;
+import com.lunarclient.apollo.event.packetenrichment.chat.ApolloPlayerChatOpenEvent;
+import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent;
+import com.lunarclient.apollo.event.player.ApolloUnregisterPlayerEvent;
+import com.lunarclient.apollo.module.button.ButtonModuleSupport;
+import com.lunarclient.apollo.module.button.ButtonSurface;
+import com.lunarclient.apollo.module.packetenrichment.PacketEnrichmentModule;
+import com.lunarclient.apollo.network.ButtonNetworkTypes;
+import com.lunarclient.apollo.option.Options;
+import com.lunarclient.apollo.option.config.Serializer;
+import com.lunarclient.apollo.player.ApolloPlayer;
import com.lunarclient.apollo.recipients.Recipients;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
import lombok.NonNull;
import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.Nullable;
/**
* Provides the chat module.
*
* @since 1.0.2
*/
-public final class ChatModuleImpl extends ChatModule {
+public final class ChatModuleImpl extends ChatModule implements ButtonSurface, Serializer {
+
+ private final ButtonModuleSupport support =
+ new ButtonModuleSupport<>(this, this, ChatModule.BROADCAST_LIVE_BUTTONS);
+
+ /**
+ * Creates a new instance of {@link ChatModuleImpl}.
+ *
+ * @since 1.2.9
+ */
+ public ChatModuleImpl() {
+ super();
+ this.serializer(ChatButton.class, new ChatButtonSerializer());
+ this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister);
+ this.handle(ApolloPlayerChatOpenEvent.class, event -> this.support.handleOpen(event.getPlayer()));
+ this.handle(ApolloPlayerChatCloseEvent.class, event -> this.support.handleClose(event.getPlayer().getUniqueId()));
+ this.handle(ApolloUnregisterPlayerEvent.class, event -> this.support.handleUnregister(event.getPlayer().getUniqueId()));
+ }
+
+ @Override
+ protected void onEnable() {
+ this.support.startBroadcast();
+ }
@Override
public void displayLiveChatMessage(@NonNull Recipients recipients, @NonNull Component text, int messageId) {
@@ -57,4 +103,126 @@ public void removeLiveChatMessage(@NonNull Recipients recipients, int messageId)
ApolloManager.getNetworkManager().sendPacket(recipients, message);
}
+ @Override
+ public void displayChatButtons(@NonNull Recipients recipients, @NonNull Collection buttons) {
+ if (buttons.size() > ChatButton.MAX_BUTTONS) {
+ throw new IllegalArgumentException("ChatButton batches support at most " + ChatButton.MAX_BUTTONS + " buttons");
+ }
+
+ this.support.displayButtons(recipients, buttons);
+ }
+
+ @Override
+ public void displayChatButton(@NonNull Recipients recipients, @NonNull ChatButton button) {
+ this.support.displayButtons(recipients, Collections.singleton(button));
+ }
+
+ @Override
+ public void removeChatButton(@NonNull Recipients recipients, @NonNull String buttonId) {
+ this.support.removeButton(recipients, buttonId);
+ }
+
+ @Override
+ public void removeChatButton(@NonNull Recipients recipients, @NonNull ChatButton button) {
+ this.support.removeButton(recipients, button.getId());
+ }
+
+ @Override
+ public void resetChatButtons(@NonNull Recipients recipients) {
+ this.support.resetButtons(recipients);
+ }
+
+ @Override
+ public void updateChatButton(@NonNull Recipients recipients, @NonNull String buttonId,
+ @NonNull ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip) {
+ this.support.pushUpdate(recipients, buttonId, content, true, tooltip);
+ }
+
+ @Override
+ public void updateChatButtonContent(@NonNull Recipients recipients, @NonNull String buttonId, @NonNull ApolloButtonContent content) {
+ this.support.pushUpdate(recipients, buttonId, content, false, null);
+ }
+
+ @Override
+ public void updateChatButtonTooltip(@NonNull Recipients recipients, @NonNull String buttonId, @Nullable ApolloButtonTooltip tooltip) {
+ this.support.pushUpdate(recipients, buttonId, null, true, tooltip);
+ }
+
+ private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
+ if (!this.isEnabled() || !this.getOptions().get(ChatModule.SEND_DEFAULT_BUTTONS)) {
+ return;
+ }
+
+ ApolloPlayer player = event.getPlayer();
+ List buttons = this.getOptions().get(player, ChatModule.DEFAULT_BUTTONS);
+ if (buttons == null || buttons.isEmpty()) {
+ return;
+ }
+
+ try {
+ this.support.displayButtons(player, buttons);
+ } catch (IllegalArgumentException exception) {
+ Apollo.getPlatform().getPlatformLogger()
+ .warning("Skipping the default chat buttons: " + exception.getMessage());
+ }
+ }
+
+ @Override
+ public void validate(ChatButton button) {
+ ButtonModuleSupport.validateCommon(button, ChatButton.BOX_WIDTH, ChatButton.BOX_HEIGHT);
+ }
+
+ @Override
+ public com.lunarclient.apollo.chat.v1.ChatButton toDisplayElement(ChatButton button, @Nullable ApolloPlayer viewer) {
+ return com.lunarclient.apollo.chat.v1.ChatButton.newBuilder()
+ .setButton(ButtonNetworkTypes.toProtobuf(button, viewer))
+ .build();
+ }
+
+ @Override
+ public Message createDisplay(List elements) {
+ return DisplayChatButtonsMessage.newBuilder()
+ .addAllChatButtons(elements)
+ .build();
+ }
+
+ @Override
+ public Message createUpdate(String buttonId, ButtonUpdate update) {
+ return UpdateChatButtonMessage.newBuilder()
+ .setId(buttonId)
+ .setUpdate(update)
+ .build();
+ }
+
+ @Override
+ public Message createRemove(String buttonId) {
+ return RemoveChatButtonMessage.newBuilder()
+ .setId(buttonId)
+ .build();
+ }
+
+ @Override
+ public Message createReset() {
+ return ResetChatButtonsMessage.getDefaultInstance();
+ }
+
+ /**
+ * Returns whether open-chat tracking is available.
+ *
+ * @return whether open-chat tracking is active
+ */
+ @Override
+ public boolean isOpenTrackingActive() {
+ PacketEnrichmentModule packetEnrichment = Apollo.getModuleManager().getModule(PacketEnrichmentModule.class);
+ if (packetEnrichment == null || !packetEnrichment.isEnabled()) {
+ return false;
+ }
+
+ Options options = packetEnrichment.getOptions();
+ return options.get(PacketEnrichmentModule.PLAYER_CHAT_OPEN_PACKET)
+ && options.get(PacketEnrichmentModule.PLAYER_CHAT_CLOSE_PACKET)
+ && options.get(PacketEnrichmentModule.PLAYER_CHAT_OPEN_EVENT)
+ && options.get(PacketEnrichmentModule.PLAYER_CHAT_CLOSE_EVENT);
+ }
+
}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java
new file mode 100644
index 00000000..505c00b6
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryButtonSerializer.java
@@ -0,0 +1,66 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.module.inventory;
+
+import com.lunarclient.apollo.common.button.ApolloButton;
+import com.lunarclient.apollo.module.button.ApolloButtonSerializer;
+import java.awt.Color;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+/**
+ * Reads and writes {@link InventoryButton}s from the module configuration;
+ * see {@link ApolloButtonSerializer} for the shared button fields.
+ *
+ * @since 1.2.9
+ */
+public final class InventoryButtonSerializer extends ApolloButtonSerializer {
+
+ @Override
+ protected ApolloButton.ApolloButtonBuilder extends InventoryButton, ?> createBuilder(ConfigurationNode node) throws SerializationException {
+ InventoryButton.InventoryButtonBuilder, ?> builder = InventoryButton.builder()
+ .inventoryType(this.parseEnum(InventoryType.class,
+ this.virtualNode(node, "inventory-type").getString(), "inventory-type"))
+ .box(this.parseEnum(InventoryButtonBox.class, this.virtualNode(node, "box").getString(), "box"));
+
+ Color background = node.node("background-color").get(Color.class);
+ if (background != null) {
+ builder.backgroundColor(background);
+ }
+
+ Color border = node.node("border-color").get(Color.class);
+ if (border != null) {
+ builder.borderColor(border);
+ }
+
+ return builder;
+ }
+
+ @Override
+ protected void serializeSurface(InventoryButton button, ConfigurationNode node) throws SerializationException {
+ node.node("inventory-type").set(button.getInventoryType().name());
+ node.node("box").set(button.getBox().name());
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java
new file mode 100644
index 00000000..87590b87
--- /dev/null
+++ b/common/src/main/java/com/lunarclient/apollo/module/inventory/InventoryModuleImpl.java
@@ -0,0 +1,221 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.module.inventory;
+
+import com.google.protobuf.Message;
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.button.v1.ButtonUpdate;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryCloseEvent;
+import com.lunarclient.apollo.event.packetenrichment.inventory.ApolloPlayerInventoryOpenEvent;
+import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent;
+import com.lunarclient.apollo.event.player.ApolloUnregisterPlayerEvent;
+import com.lunarclient.apollo.inventory.v1.DisplayInventoryButtonsMessage;
+import com.lunarclient.apollo.inventory.v1.RemoveInventoryButtonMessage;
+import com.lunarclient.apollo.inventory.v1.ResetInventoryButtonsMessage;
+import com.lunarclient.apollo.inventory.v1.UpdateInventoryButtonMessage;
+import com.lunarclient.apollo.module.button.ButtonModuleSupport;
+import com.lunarclient.apollo.module.button.ButtonSurface;
+import com.lunarclient.apollo.module.packetenrichment.PacketEnrichmentModule;
+import com.lunarclient.apollo.network.ButtonNetworkTypes;
+import com.lunarclient.apollo.option.Options;
+import com.lunarclient.apollo.option.config.Serializer;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import com.lunarclient.apollo.recipients.Recipients;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import lombok.NonNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides the inventory module.
+ *
+ * @since 1.2.9
+ */
+public final class InventoryModuleImpl extends InventoryModule implements ButtonSurface, Serializer {
+
+ private final ButtonModuleSupport support =
+ new ButtonModuleSupport<>(this, this, InventoryModule.BROADCAST_LIVE_BUTTONS);
+
+ /**
+ * Creates a new instance of {@link InventoryModuleImpl}.
+ *
+ * @since 1.2.9
+ */
+ public InventoryModuleImpl() {
+ super();
+ this.serializer(InventoryButton.class, new InventoryButtonSerializer());
+ this.handle(ApolloRegisterPlayerEvent.class, this::onPlayerRegister);
+ this.handle(ApolloPlayerInventoryOpenEvent.class, event -> this.support.handleOpen(event.getPlayer()));
+ this.handle(ApolloPlayerInventoryCloseEvent.class, event -> this.support.handleClose(event.getPlayer().getUniqueId()));
+ this.handle(ApolloUnregisterPlayerEvent.class, event -> this.support.handleUnregister(event.getPlayer().getUniqueId()));
+ }
+
+ @Override
+ protected void onEnable() {
+ this.support.startBroadcast();
+ }
+
+ @Override
+ public void displayInventoryButtons(@NonNull Recipients recipients, @NonNull Collection buttons) {
+ int leftButtons = 0;
+ int rightButtons = 0;
+ for (InventoryButton button : buttons) {
+ if (button.getBox() == InventoryButtonBox.LEFT) {
+ leftButtons++;
+ } else if (button.getBox() == InventoryButtonBox.RIGHT) {
+ rightButtons++;
+ }
+ }
+
+ if (leftButtons > InventoryButton.MAX_BUTTONS_PER_BOX || rightButtons > InventoryButton.MAX_BUTTONS_PER_BOX) {
+ throw new IllegalArgumentException("InventoryButton batches support at most " + InventoryButton.MAX_BUTTONS_PER_BOX + " buttons per box");
+ }
+
+ this.support.displayButtons(recipients, buttons);
+ }
+
+ @Override
+ public void displayInventoryButton(@NonNull Recipients recipients, @NonNull InventoryButton button) {
+ this.support.displayButtons(recipients, Collections.singleton(button));
+ }
+
+ @Override
+ public void removeInventoryButton(@NonNull Recipients recipients, @NonNull String buttonId) {
+ this.support.removeButton(recipients, buttonId);
+ }
+
+ @Override
+ public void removeInventoryButton(@NonNull Recipients recipients, @NonNull InventoryButton button) {
+ this.support.removeButton(recipients, button.getId());
+ }
+
+ @Override
+ public void resetInventoryButtons(@NonNull Recipients recipients) {
+ this.support.resetButtons(recipients);
+ }
+
+ @Override
+ public void updateInventoryButton(@NonNull Recipients recipients, @NonNull String buttonId,
+ @NonNull ApolloButtonContent content, @Nullable ApolloButtonTooltip tooltip) {
+ this.support.pushUpdate(recipients, buttonId, content, true, tooltip);
+ }
+
+ @Override
+ public void updateInventoryButtonContent(@NonNull Recipients recipients, @NonNull String buttonId, @NonNull ApolloButtonContent content) {
+ this.support.pushUpdate(recipients, buttonId, content, false, null);
+ }
+
+ @Override
+ public void updateInventoryButtonTooltip(@NonNull Recipients recipients, @NonNull String buttonId, @Nullable ApolloButtonTooltip tooltip) {
+ this.support.pushUpdate(recipients, buttonId, null, true, tooltip);
+ }
+
+ private void onPlayerRegister(ApolloRegisterPlayerEvent event) {
+ if (!this.isEnabled() || !this.getOptions().get(InventoryModule.SEND_DEFAULT_BUTTONS)) {
+ return;
+ }
+
+ ApolloPlayer player = event.getPlayer();
+ List buttons = this.getOptions().get(player, InventoryModule.DEFAULT_BUTTONS);
+ if (buttons == null || buttons.isEmpty()) {
+ return;
+ }
+
+ try {
+ this.support.displayButtons(player, buttons);
+ } catch (IllegalArgumentException exception) {
+ Apollo.getPlatform().getPlatformLogger()
+ .warning("Skipping the default inventory buttons: " + exception.getMessage());
+ }
+ }
+
+ @Override
+ public void validate(InventoryButton button) {
+ ButtonModuleSupport.validateCommon(button, InventoryButton.BOX_WIDTH, InventoryButton.BOX_HEIGHT);
+
+ ButtonModuleSupport.requireSet(button.getInventoryType(), "InventoryButton#inventoryType");
+ ButtonModuleSupport.requireSet(button.getBox(), "InventoryButton#box");
+ }
+
+ @Override
+ public com.lunarclient.apollo.inventory.v1.InventoryButton toDisplayElement(InventoryButton button, @Nullable ApolloPlayer viewer) {
+ return com.lunarclient.apollo.inventory.v1.InventoryButton.newBuilder()
+ .setButton(ButtonNetworkTypes.toProtobuf(button, viewer))
+ .setInventoryType(com.lunarclient.apollo.inventory.v1.InventoryType
+ .forNumber(button.getInventoryType().ordinal() + 1))
+ .setBox(com.lunarclient.apollo.inventory.v1.InventoryButtonBox
+ .forNumber(button.getBox().ordinal() + 1))
+ .build();
+ }
+
+ @Override
+ public Message createDisplay(List elements) {
+ return DisplayInventoryButtonsMessage.newBuilder()
+ .addAllInventoryButtons(elements)
+ .build();
+ }
+
+ @Override
+ public Message createUpdate(String buttonId, ButtonUpdate update) {
+ return UpdateInventoryButtonMessage.newBuilder()
+ .setId(buttonId)
+ .setUpdate(update)
+ .build();
+ }
+
+ @Override
+ public Message createRemove(String buttonId) {
+ return RemoveInventoryButtonMessage.newBuilder()
+ .setId(buttonId)
+ .build();
+ }
+
+ @Override
+ public Message createReset() {
+ return ResetInventoryButtonsMessage.getDefaultInstance();
+ }
+
+ /**
+ * Returns whether open-inventory tracking is available.
+ *
+ * @return whether open-inventory tracking is active
+ */
+ @Override
+ public boolean isOpenTrackingActive() {
+ PacketEnrichmentModule packetEnrichment = Apollo.getModuleManager().getModule(PacketEnrichmentModule.class);
+ if (packetEnrichment == null || !packetEnrichment.isEnabled()) {
+ return false;
+ }
+
+ Options options = packetEnrichment.getOptions();
+ return options.get(PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_PACKET)
+ && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_PACKET)
+ && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_OPEN_EVENT)
+ && options.get(PacketEnrichmentModule.PLAYER_INVENTORY_CLOSE_EVENT);
+ }
+
+}
diff --git a/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java b/common/src/main/java/com/lunarclient/apollo/module/packetenrichment/PacketEnrichmentImpl.java
index 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/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java b/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java
index 4462a872..fbd7c9ca 100644
--- a/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java
+++ b/common/src/main/java/com/lunarclient/apollo/option/config/CommonSerializers.java
@@ -23,8 +23,19 @@
*/
package com.lunarclient.apollo.option.config;
+import com.lunarclient.apollo.common.icon.AdvancedResourceLocationIcon;
+import com.lunarclient.apollo.common.icon.CustomModelData;
+import com.lunarclient.apollo.common.icon.Icon;
+import com.lunarclient.apollo.common.icon.ItemStackIcon;
+import com.lunarclient.apollo.common.icon.ResourceLocationIcon;
+import com.lunarclient.apollo.common.icon.SimpleResourceLocationIcon;
+import com.lunarclient.apollo.common.profile.Profile;
import java.awt.Color;
import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
@@ -43,7 +54,11 @@ public final class CommonSerializers implements Serializer {
* @since 1.0.0
*/
public CommonSerializers() {
+ ItemStackIconSerializer itemStackIconSerializer = new ItemStackIconSerializer();
+
this.serializer(Color.class, new ColorSerializer());
+ this.serializer(Icon.class, new IconSerializer(itemStackIconSerializer));
+ this.serializer(ItemStackIcon.class, itemStackIconSerializer);
}
private static final class ColorSerializer implements TypeSerializer {
@@ -87,4 +102,197 @@ public void serialize(Type type, @Nullable Color color, ConfigurationNode node)
}
}
+ @RequiredArgsConstructor
+ private static final class IconSerializer implements TypeSerializer {
+
+ private final ItemStackIconSerializer itemStackIconSerializer;
+
+ @Override
+ public Icon deserialize(Type type, ConfigurationNode node) throws SerializationException {
+ if (node.hasChild("name") || node.hasChild("id")) {
+ return this.itemStackIconSerializer.deserialize(type, node);
+ }
+
+ if (!node.hasChild("resource-location")) {
+ throw new SerializationException("Icons require a 'name', 'id' or 'resource-location' field!");
+ }
+
+ String resourceLocation = node.node("resource-location").getString();
+ if (resourceLocation == null || resourceLocation.isEmpty()) {
+ throw new SerializationException("Icon 'resource-location' must not be empty!");
+ }
+
+ if (node.hasChild("width") || node.hasChild("height") || node.hasChild("min-u")
+ || node.hasChild("max-u") || node.hasChild("min-v") || node.hasChild("max-v")) {
+ return AdvancedResourceLocationIcon.builder()
+ .resourceLocation(resourceLocation)
+ .width((float) node.node("width").getDouble())
+ .height((float) node.node("height").getDouble())
+ .minU((float) node.node("min-u").getDouble())
+ .maxU((float) node.node("max-u").getDouble(1.0D))
+ .minV((float) node.node("min-v").getDouble())
+ .maxV((float) node.node("max-v").getDouble(1.0D))
+ .build();
+ }
+
+ if (node.hasChild("size")) {
+ return SimpleResourceLocationIcon.builder()
+ .resourceLocation(resourceLocation)
+ .size(node.node("size").getInt())
+ .build();
+ }
+
+ return ResourceLocationIcon.builder()
+ .resourceLocation(resourceLocation)
+ .build();
+ }
+
+ @Override
+ public void serialize(Type type, @Nullable Icon icon, ConfigurationNode node) throws SerializationException {
+ if (icon == null) {
+ node.raw(null);
+ return;
+ }
+
+ if (icon instanceof ItemStackIcon) {
+ this.itemStackIconSerializer.serialize(type, (ItemStackIcon) icon, node);
+ return;
+ }
+
+ if (icon instanceof SimpleResourceLocationIcon) {
+ SimpleResourceLocationIcon simple = (SimpleResourceLocationIcon) icon;
+ node.node("resource-location").set(simple.getResourceLocation());
+ node.node("size").set(simple.getSize());
+ return;
+ }
+
+ if (icon instanceof AdvancedResourceLocationIcon) {
+ AdvancedResourceLocationIcon advanced = (AdvancedResourceLocationIcon) icon;
+ node.node("resource-location").set(advanced.getResourceLocation());
+ node.node("width").set((double) advanced.getWidth());
+ node.node("height").set((double) advanced.getHeight());
+ node.node("min-u").set((double) advanced.getMinU());
+ node.node("max-u").set((double) advanced.getMaxU());
+ node.node("min-v").set((double) advanced.getMinV());
+ node.node("max-v").set((double) advanced.getMaxV());
+ return;
+ }
+
+ if (icon instanceof ResourceLocationIcon) {
+ node.node("resource-location").set(((ResourceLocationIcon) icon).getResourceLocation());
+ return;
+ }
+
+ throw new SerializationException("Unknown icon type: " + icon.getClass().getName());
+ }
+ }
+
+ private static final class ItemStackIconSerializer implements TypeSerializer {
+ @Override
+ public ItemStackIcon deserialize(Type type, ConfigurationNode node) throws SerializationException {
+ if (!node.hasChild("name") && !node.hasChild("id")) {
+ throw new SerializationException("Item icons require a 'name' or 'id' field!");
+ }
+
+ ItemStackIcon.ItemStackIconBuilder builder = ItemStackIcon.builder();
+ if (node.hasChild("name")) {
+ String name = node.node("name").getString();
+ if (name == null || name.isEmpty()) {
+ throw new SerializationException("Item icon 'name' must not be empty!");
+ }
+
+ builder.itemName(name);
+ } else {
+ builder.itemId(node.node("id").getInt());
+ }
+
+ if (node.hasChild("custom-model-data")) {
+ builder.customModelData(node.node("custom-model-data").getInt());
+ }
+
+ if (node.hasChild("model-data")) {
+ ConfigurationNode data = node.node("model-data");
+ builder.customModelDataObject(CustomModelData.builder()
+ .floats(data.node("floats").getList(Float.class, Collections.emptyList()))
+ .flags(data.node("flags").getList(Boolean.class, Collections.emptyList()))
+ .strings(data.node("strings").getList(String.class, Collections.emptyList()))
+ .colors(data.node("colors").getList(Integer.class, Collections.emptyList()))
+ .build());
+ }
+
+ if (node.hasChild("potion")) {
+ builder.potion(node.node("potion").getString());
+ }
+
+ if (node.hasChild("profile")) {
+ builder.profile(this.readProfile(node.node("profile")));
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public void serialize(Type type, @Nullable ItemStackIcon icon, ConfigurationNode node) throws SerializationException {
+ if (icon == null) {
+ node.raw(null);
+ return;
+ }
+
+ if (icon.getItemName() != null) {
+ node.node("name").set(icon.getItemName());
+ } else {
+ node.node("id").set(icon.getItemId());
+ }
+
+ if (icon.getCustomModelData() != 0) {
+ node.node("custom-model-data").set(icon.getCustomModelData());
+ }
+
+ CustomModelData modelData = icon.getCustomModelDataObject();
+ if (modelData != null) {
+ this.writeList(node.node("model-data", "floats"), Float.class, modelData.getFloats());
+ this.writeList(node.node("model-data", "flags"), Boolean.class, modelData.getFlags());
+ this.writeList(node.node("model-data", "strings"), String.class, modelData.getStrings());
+ this.writeList(node.node("model-data", "colors"), Integer.class, modelData.getColors());
+ }
+
+ if (icon.getPotion() != null) {
+ node.node("potion").set(icon.getPotion());
+ }
+
+ Profile profile = icon.getProfile();
+ if (profile != null) {
+ if (profile.getId() != null) {
+ node.node("profile", "id").set(profile.getId().toString());
+ }
+
+ node.node("profile", "texture").set(profile.getTexture());
+ node.node("profile", "signature").set(profile.getSignature());
+ }
+ }
+
+ private Profile readProfile(ConfigurationNode node) throws SerializationException {
+ Profile.ProfileBuilder builder = Profile.builder()
+ .texture(node.node("texture").getString(""))
+ .signature(node.node("signature").getString(""));
+
+ String id = node.node("id").getString();
+ if (id != null) {
+ try {
+ builder.id(UUID.fromString(id));
+ } catch (IllegalArgumentException exception) {
+ throw new SerializationException("Invalid profile id '" + id + "'!");
+ }
+ }
+
+ return builder.build();
+ }
+
+ private void writeList(ConfigurationNode node, Class type, List values) throws SerializationException {
+ if (!values.isEmpty()) {
+ node.setList(type, values);
+ }
+ }
+ }
+
}
diff --git a/docs/developers/events.mdx b/docs/developers/events.mdx
index a8899eee..358b356a 100644
--- a/docs/developers/events.mdx
+++ b/docs/developers/events.mdx
@@ -218,6 +218,36 @@ _Called when the player opens their chat._
+
+ApolloPlayerInventoryCloseEvent
+
+### ApolloPlayerInventoryCloseEvent
+
+_Called when the player closes their inventory._
+
+| Field | Description |
+| -------------------------- | -------------------------------------------------- |
+| `ApolloPlayer player` | The Apollo player that sent the packet. |
+| `long instantiationTimeMs` | The unix timestamp when the packet was created. |
+| `PlayerInfo playerInfo` | The player's general information. |
+
+
+
+
+ApolloPlayerInventoryOpenEvent
+
+### ApolloPlayerInventoryOpenEvent
+
+_Called when the player opens their inventory._
+
+| Field | Description |
+| -------------------------- | -------------------------------------------------- |
+| `ApolloPlayer player` | The Apollo player that sent the packet. |
+| `long instantiationTimeMs` | The unix timestamp when the packet was created. |
+| `PlayerInfo playerInfo` | The player's general information. |
+
+
+
ApolloPlayerAttackEvent
diff --git a/docs/developers/lightweight/json/serverbound-packets.mdx b/docs/developers/lightweight/json/serverbound-packets.mdx
index fc86cf12..0cc4ad8a 100644
--- a/docs/developers/lightweight/json/serverbound-packets.mdx
+++ b/docs/developers/lightweight/json/serverbound-packets.mdx
@@ -54,6 +54,10 @@ public class ApolloPacketReceiveJsonListener implements PluginMessageListener {
this.onPlayerChatOpen(payload);
} else if ("lunarclient.apollo.packetenrichment.v1.PlayerChatCloseMessage".equals(type)) {
this.onPlayerChatClose(payload);
+ } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryOpenMessage".equals(type)) {
+ this.onPlayerInventoryOpen(player, payload);
+ } else if ("lunarclient.apollo.packetenrichment.v1.PlayerInventoryCloseMessage".equals(type)) {
+ this.onPlayerInventoryClose(player, payload);
} else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemMessage".equals(type)) {
this.onPlayerUseItem(payload);
} else if ("lunarclient.apollo.packetenrichment.v1.PlayerUseItemBucketMessage".equals(type)) {
@@ -102,6 +106,26 @@ public class ApolloPacketReceiveJsonListener implements PluginMessageListener {
this.onPlayerInfo(message.getAsJsonObject("player_info"));
}
+ private void onPlayerInventoryOpen(Player player, JsonObject message) {
+ long instantiationTimeMs = JsonUtil.toJavaTimestamp(message);
+ this.onPlayerInfo(message.getAsJsonObject("player_info"));
+
+ InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample();
+ if (example instanceof InventoryJsonExample) {
+ ((InventoryJsonExample) example).handleInventoryOpen(player);
+ }
+ }
+
+ private void onPlayerInventoryClose(Player player, JsonObject message) {
+ long instantiationTimeMs = JsonUtil.toJavaTimestamp(message);
+ this.onPlayerInfo(message.getAsJsonObject("player_info"));
+
+ InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample();
+ if (example instanceof InventoryJsonExample) {
+ ((InventoryJsonExample) example).handleInventoryClose(player);
+ }
+ }
+
private void onPlayerUseItem(JsonObject message) {
long instantiationTimeMs = JsonUtil.toJavaTimestamp(message);
this.onPlayerInfo(message.getAsJsonObject("player_info"));
diff --git a/docs/developers/lightweight/protobuf/serverbound-packets.mdx b/docs/developers/lightweight/protobuf/serverbound-packets.mdx
index 6690301b..8a75188a 100644
--- a/docs/developers/lightweight/protobuf/serverbound-packets.mdx
+++ b/docs/developers/lightweight/protobuf/serverbound-packets.mdx
@@ -31,6 +31,10 @@ public class ApolloPacketReceiveProtoListener implements PluginMessageListener {
this.onPlayerChatOpen(any.unpack(PlayerChatOpenMessage.class));
} else if (any.is(PlayerChatCloseMessage.class)) {
this.onPlayerChatClose(any.unpack(PlayerChatCloseMessage.class));
+ } else if (any.is(PlayerInventoryOpenMessage.class)) {
+ this.onPlayerInventoryOpen(player, any.unpack(PlayerInventoryOpenMessage.class));
+ } else if (any.is(PlayerInventoryCloseMessage.class)) {
+ this.onPlayerInventoryClose(player, any.unpack(PlayerInventoryCloseMessage.class));
} else if (any.is(PlayerUseItemMessage.class)) {
this.onPlayerUseItem(any.unpack(PlayerUseItemMessage.class));
} else if (any.is(PlayerUseItemBucketMessage.class)) {
@@ -83,6 +87,30 @@ public class ApolloPacketReceiveProtoListener implements PluginMessageListener {
this.onPlayerInfo(playerInfo);
}
+ private void onPlayerInventoryOpen(Player player, PlayerInventoryOpenMessage message) {
+ long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime());
+
+ PlayerInfo playerInfo = message.getPlayerInfo();
+ this.onPlayerInfo(playerInfo);
+
+ InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample();
+ if (example instanceof InventoryProtoExample) {
+ ((InventoryProtoExample) example).handleInventoryOpen(player);
+ }
+ }
+
+ private void onPlayerInventoryClose(Player player, PlayerInventoryCloseMessage message) {
+ long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime());
+
+ PlayerInfo playerInfo = message.getPlayerInfo();
+ this.onPlayerInfo(playerInfo);
+
+ InventoryExample example = ApolloExamplePlugin.getInstance().getInventoryExample();
+ if (example instanceof InventoryProtoExample) {
+ ((InventoryProtoExample) example).handleInventoryClose(player);
+ }
+ }
+
private void onPlayerUseItem(PlayerUseItemMessage message) {
long instantiationTimeMs = ProtobufUtil.toJavaTimestamp(message.getPacketInfo().getInstantiationTime());
diff --git a/docs/developers/modules/chat.mdx b/docs/developers/modules/chat.mdx
index 6065ed97..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.

@@ -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/modules/packetenrichment.mdx b/docs/developers/modules/packetenrichment.mdx
index 3aba0e1c..728cf986 100644
--- a/docs/developers/modules/packetenrichment.mdx
+++ b/docs/developers/modules/packetenrichment.mdx
@@ -30,6 +30,8 @@ The majority of this module is handled through Apollo's [event system](/apollo/d
The following events are related to the packet enrichment module, you can find more information about each event on the [events page](/apollo/developers/events).
* `ApolloPlayerChatCloseEvent`
* `ApolloPlayerChatOpenEvent`
+* `ApolloPlayerInventoryCloseEvent`
+* `ApolloPlayerInventoryOpenEvent`
* `ApolloPlayerAttackEvent`
* `ApolloPlayerUseItemEvent`
* `ApolloPlayerUseItemBucketEvent`
@@ -89,6 +91,32 @@ Visit [Apollo Serverbound packets](/apollo/developers/lightweight/protobuf/serve
- Type: `Boolean`
- Default: `false`
+- __`PLAYER_INVENTORY_OPEN_PACKET`__
+ - Controls whether the client sends an additional player inventory open packet to the server.
+ - Values
+ - Type: `Boolean`
+ - Default: `false`
+
+- __`PLAYER_INVENTORY_OPEN_EVENT`__
+ - Controls whether Apollo fires `ApolloPlayerInventoryOpenEvent` when the packet is received.
+ - Disable this and handle the packet yourself if you require asynchronous or off-thread processing.
+ - Values
+ - Type: `Boolean`
+ - Default: `false`
+
+- __`PLAYER_INVENTORY_CLOSE_PACKET`__
+ - Controls whether the client sends an additional player inventory close packet to the server.
+ - Values
+ - Type: `Boolean`
+ - Default: `false`
+
+- __`PLAYER_INVENTORY_CLOSE_EVENT`__
+ - Controls whether Apollo fires `ApolloPlayerInventoryCloseEvent` when the packet is received.
+ - Disable this and handle the packet yourself if you require asynchronous or off-thread processing.
+ - Values
+ - Type: `Boolean`
+ - Default: `false`
+
- __`PLAYER_USE_ITEM_PACKET`__
- Controls whether the client sends an additional player use item packet to the server.
- Values
diff --git a/docs/developers/utilities/_meta.json b/docs/developers/utilities/_meta.json
index 9007de2b..8f08d627 100644
--- a/docs/developers/utilities/_meta.json
+++ b/docs/developers/utilities/_meta.json
@@ -1,4 +1,5 @@
{
+ "buttons": "Buttons",
"colors": "Colors",
"cuboids": "Cuboids",
"icons": "Icons",
diff --git a/docs/developers/utilities/buttons.mdx b/docs/developers/utilities/buttons.mdx
new file mode 100644
index 00000000..d85e985a
--- /dev/null
+++ b/docs/developers/utilities/buttons.mdx
@@ -0,0 +1,306 @@
+import { Callout } from 'nextra-theme-docs'
+
+# Buttons
+
+## Overview
+
+Apollo provides a shared button system in the `com.lunarclient.apollo.common.button` package. The abstract
+`ApolloButton` carries everything a clickable GUI button needs: content, tooltip, colors, shape, size and a
+click action; independently of where the button is displayed. Each *surface* then defines the placement.
+
+Two surfaces are available today: the [inventory module](/apollo/developers/modules/inventory)'s inventory
+buttons, placed in two fixed-size boxes on each side of the player inventory, and the
+[chat module](/apollo/developers/modules/chat)'s chat buttons, placed in a strip between the chat input and
+the chat log.
+
+Buttons are always built through a surface type (e.g. `InventoryButton.builder()`), which adds the surface's
+placement properties and defines the container the button `position` is measured against. There is no
+standalone `ApolloButton.builder()`.
+
+## `ApolloButton` Properties
+
+Every surface builder inherits the following properties from `ApolloButton`.
+
+```java
+public abstract class ApolloButton {
+
+
+ /**
+ * Returns the button {@link String} id, unique within a single
+ * display batch.
+ *
+ * Displaying another button with the same id replaces the
+ * previous one.
+ *
+ * @since 1.2.9
+ */
+ @NotNull String id;
+
+ /**
+ * Returns the {@link HudPosition} of this button, relative to the
+ * top-left corner of the container it is placed in.
+ *
+ * The button must fit inside the container: {@code 0 <= x},
+ * {@code 0 <= y}, {@code x + width <= container width} and
+ * {@code y + height <= container height}. See the surface type for
+ * its container dimensions (e.g. {@code InventoryButton#BOX_WIDTH}).
+ *
+ * @since 1.2.9
+ */
+ @NotNull HudPosition position;
+
+ /**
+ * Returns the {@link ApolloButtonSize} of this button.
+ *
+ * Use one of the surface's suggested sizes (e.g.
+ * {@code InventoryButton.SIZE_MEDIUM}) or a fully custom
+ * {@link ApolloButtonSize#of(float, float)}.
+ *
+ * @return the button size
+ * @since 1.2.9
+ */
+ @NotNull ApolloButtonSize size;
+
+ /**
+ * Returns the {@link ApolloButtonShape} of this button.
+ *
+ * @since 1.2.9
+ */
+ @NotNull ApolloButtonShape shape;
+
+ /**
+ * Returns the {@link ApolloButtonContent} rendered inside this button.
+ *
+ * Built via {@code ApolloButtonContent.builder()}, appending static
+ * components, icons or live per-player parts that are re-resolved
+ * whenever the content is sent (see the owning module's live button
+ * broadcast option, e.g. {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).
+ *
+ * @return the button content
+ * @since 1.2.9
+ */
+ @NotNull ApolloButtonContent content;
+
+ /**
+ * Returns the {@link ApolloButtonTooltip} shown while this button is hovered.
+ *
+ * Built via {@code ApolloButtonTooltip.of(...)} for static lines,
+ * or {@code ApolloButtonTooltip.live(...)} for per-player lines that
+ * are re-resolved whenever the button content is sent (see the owning
+ * module's live button broadcast option, e.g.
+ * {@code InventoryModule#BROADCAST_LIVE_BUTTONS}).
+ *
+ * @return the tooltip, or {@code null} for no tooltip
+ * @since 1.2.9
+ */
+ @Builder.Default
+ @Nullable ApolloButtonTooltip tooltip = null;
+
+ /**
+ * Returns the {@link ApolloButtonAction} executed when this button
+ * is clicked.
+ *
+ * Built via {@link ApolloButtonAction#runCommand(String)},
+ * {@link ApolloButtonAction#openUrl(String)} or
+ * {@link ApolloButtonAction#clientAction(ApolloButtonClientAction)}.
+ *
+ * @return the click action, or {@code null}
+ * @since 1.2.9
+ */
+ @Builder.Default
+ @Nullable ApolloButtonAction onClick = null;
+
+ /**
+ * Returns the background {@link Color} used while this button is hovered.
+ *
+ * @return the hovered background color, or {@code null}
+ * @since 1.2.9
+ */
+ @Builder.Default
+ @Nullable Color hoveredBackgroundColor = null;
+
+ /**
+ * Returns the border {@link Color} used while this button is hovered.
+ *
+ * @return the hovered border color, or {@code null}
+ * @since 1.2.9
+ */
+ @Builder.Default
+ @Nullable Color hoveredBorderColor = null;
+
+ /**
+ * Returns the background {@link Color} of this button.
+ *
+ * @return the background color
+ * @since 1.2.9
+ */
+ public abstract Color getBackgroundColor();
+
+ /**
+ * Returns the border {@link Color} of this button.
+ *
+ * @return the border color
+ * @since 1.2.9
+ */
+ public abstract Color getBorderColor();
+
+}
+```
+
+### Sample Code
+
+The chain below builds an inventory button; `.box(...)` is the inventory surface's placement property, while
+everything else is inherited from `ApolloButton`.
+
+```java
+public void displayButtonExample(Player viewer) {
+ Optional apolloPlayerOpt = Apollo.getPlayerManager().getPlayer(viewer.getUniqueId());
+
+ apolloPlayerOpt.ifPresent(apolloPlayer -> {
+ InventoryButton vote = InventoryButton.builder()
+ .id("vote")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 52))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Vote"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Vote", NamedTextColor.GREEN),
+ Component.text("Vote daily for rewards", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.openUrl("https://example.com/vote"))
+ .build();
+
+ this.inventoryModule.displayInventoryButton(apolloPlayer, vote);
+ });
+}
+```
+
+## `ApolloButtonContent` Builder
+
+Button content is an ordered sequence of adventure components and icons, rendered as a single centered row.
+At least one part is required, and content supports at most 30 parts (`ApolloButtonContent.MAX_PARTS`)
+
+`.append(Component)` appends an adventure component to the content row.
+```java
+.append(Component.text("Vote for us!"))
+```
+
+`.append(Icon)` appends an icon to the content row. Read the [icons utilities page](/apollo/developers/utilities/icons) to learn more about icons.
+```java
+.append(ItemStackIcon.builder().itemName("NETHER_STAR").build())
+```
+
+`.append(Function, Duration)` appends a live part, resolved into a component for each viewing
+player whenever the content is sent: at the owning module's live button broadcast (e.g. the inventory module's [live button broadcast](/apollo/developers/modules/inventory#live-button-broadcast))
+and refreshed at the given interval. The interval must be positive and is quantized to server ticks (50ms).
+Shorthand for `append(ApolloButtonContentPart.live(resolver, updateInterval))`.
+```java
+.append(apolloViewer -> Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN), Duration.ofMillis(2500L))
+```
+
+`.append(ApolloButtonContentPart)` appends a pre-built content part; see the `ApolloButtonContentPart` factories (`component(...)`, `icon(...)`, `live(...)`).
+```java
+.append(ApolloButtonContentPart.live(
+ apolloViewer -> Component.text(getBalance(apolloViewer.getUniqueId()), NamedTextColor.GOLD),
+ Duration.ofMillis(2500L)))
+```
+
+`.scale(float)` sets the scale factor applied to the content row. Defaults to `1.0` and must be between `0.25` and `4`.
+```java
+.scale(1.0F)
+```
+
+### Sample Code
+
+```java
+public static ApolloButtonContent buttonContentExample() {
+ return ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder().itemName("GOLD_INGOT").build())
+ .append(Component.text("Balance: ", NamedTextColor.GRAY))
+ .append(ApolloButtonContentPart.live(
+ apolloViewer -> Component.text(getBalance(apolloViewer.getUniqueId()), NamedTextColor.GOLD),
+ Duration.ofMillis(2500L)))
+ .scale(1.0F)
+ .build();
+}
+```
+
+## `ApolloButtonTooltip`
+
+The tooltip shown while a button is hovered, rendered like a vanilla item tooltip at the mouse. Built via
+`ApolloButtonTooltip.of(...)` for static lines (accepting either varargs or a `List`), or
+`ApolloButtonTooltip.live(resolver, Duration)` for a per-player resolver that is re-resolved whenever the
+button content is sent, refreshed at the given interval. The interval must be positive and is quantized to
+server ticks (50ms). Tooltips support at most 100 lines (`ApolloButtonTooltip.MAX_LINES`).
+
+```java
+ApolloButtonTooltip.of(
+ Component.text("Shop", NamedTextColor.GREEN),
+ Component.text("Browse the server shop", NamedTextColor.GRAY));
+
+ApolloButtonTooltip.live(apolloViewer -> Arrays.asList(
+ Component.text("Updated " + LocalTime.now(), NamedTextColor.DARK_GRAY)),
+ Duration.ofMillis(2500L));
+```
+
+## `ApolloButtonAction`
+
+The action executed when a button is clicked. A button without an action is purely decorative. Built via one
+of three factories, each returning a dedicated `ApolloButtonAction` subtype (`RunCommandAction`, `OpenUrlAction`, `ClientAction`):
+
+- `ApolloButtonAction.runCommand(String)` runs the command as the player. Must start with `/`. The client
+ shows a confirmation prompt before running the command.
+- `ApolloButtonAction.openUrl(String)` opens the URL. Respects the player's chat links setting and Apollo
+ link-prompt settings.
+- `ApolloButtonAction.clientAction(ApolloButtonClientAction)` executes a built-in client action:
+ `OPEN_MINIMAP_VIEW` opens Lunar's fullscreen minimap view (does nothing when the player has the MiniMap mod
+ disabled) or `OPEN_WAYPOINTS_MENU` opens the waypoints menu.
+
+```java
+.onClick(ApolloButtonAction.runCommand("/shop"))
+.onClick(ApolloButtonAction.openUrl("https://example.com/vote"))
+.onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW))
+```
+
+## `ApolloButtonShape`
+
+The button shape, either `ROUNDED_SQUARE` or `CIRCLE`. Rounded squares render with slightly rounded corners;
+circles use the smaller of the button's `width` and `height` as their diameter, centered within the button
+bounds.
+
+```java
+.shape(ApolloButtonShape.ROUNDED_SQUARE)
+```
+
+## `ApolloButtonSize`
+
+The button size, in GUI-scaled pixels, created via `ApolloButtonSize.of(width, height)`.
+
+```java
+.size(ApolloButtonSize.of(56, 24))
+.size(ApolloButtonSize.of(40))
+```
+
+Suggested size presets are per-surface, tuned to the surface's container dimensions; for inventory buttons they live
+on `InventoryButton` (`SIZE_SMALL`, `SIZE_MEDIUM`, `SIZE_LARGE`, `SIZE_WIDE`) and for chat buttons on `ChatButton` (`SIZE_SMALL`, `SIZE_MEDIUM`, `SIZE_ICON`);
+see the [inventory module page](/apollo/developers/modules/inventory#inventorybutton-options) and the [chat module page](/apollo/developers/modules/chat#chat-buttons).
+
+## Updating Buttons
+
+Modules expose update methods that replace the content and/or tooltip of a previously displayed button,
+leaving all other button properties unchanged (e.g. `InventoryModule#updateInventoryButton`, which takes an
+`ApolloButtonContent` and an `ApolloButtonTooltip`). Buttons are matched by id.
+
+The partial-update semantics are the same for every surface:
+
+- An unset `content` keeps the previous content; its parts and its scale; a present content replaces both.
+- An unset `tooltip` keeps the previous tooltip; a present tooltip fully replaces it, and a present tooltip with zero lines clears it.
+
+Live content parts and live tooltips are resolved per recipient before sending, both on manual updates and by the owning
+module's automatic live button broadcast. The broadcast refreshes content and tooltip independently, each on its own update interval.
diff --git a/docs/developers/utilities/icons.mdx b/docs/developers/utilities/icons.mdx
index 28eba917..1d29d9e3 100644
--- a/docs/developers/utilities/icons.mdx
+++ b/docs/developers/utilities/icons.mdx
@@ -9,7 +9,9 @@ Apollo adds three different icon builders, `ItemStackIcon`, `SimpleResourceLocat
## `ItemStackIcon` Builder
The `ItemStackIcon` builder is used to assign an icon using a specified ItemStack name or ID. This will utilize the texture present in the player's resource pack as the icon.
-If you're using a custom resource pack and want to make the icon appear as a model, you can set the `customModelData` to the models data value. On versions below 1.13 `customModelData` sets the durability instead.
+If you're using a custom resource pack and want to make the icon appear as a model, you can set the `customModelDataObject` field to a `CustomModelData` object holding the model's data values. On versions below 1.13 the custom model data sets the durability instead.
+
+For potion items, set `potion` to a potion id e.g. `ItemStackIcon.builder().itemName("SPLASH_POTION").potion("healing").build()`.
If your server accepts players on Minecraft versions higher than `1.8.8` then you need to use `itemName`.
@@ -50,6 +52,14 @@ public final class ItemStackIcon extends Icon {
*/
@Nullable Profile profile;
+ /**
+ * Returns the icon {@link String} potion id (e.g. {@code "healing"}).
+ *
+ * @return the icon potion id
+ * @since 1.2.9
+ */
+ @Nullable String potion;
+
}
```
diff --git a/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..8bb5dace
--- /dev/null
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MenuLayout.java
@@ -0,0 +1,282 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.example.api.module.inventorybuttons;
+
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.common.button.ApolloButtonShape;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.action.ApolloButtonAction;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContentPart;
+import com.lunarclient.apollo.common.icon.ItemStackIcon;
+import com.lunarclient.apollo.common.location.HudPosition;
+import com.lunarclient.apollo.common.profile.Profile;
+import com.lunarclient.apollo.module.inventory.InventoryButton;
+import com.lunarclient.apollo.module.inventory.InventoryButtonBox;
+import com.lunarclient.apollo.module.inventory.InventoryModule;
+import com.lunarclient.apollo.module.inventory.InventoryType;
+import java.awt.Color;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.UUID;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Player;
+
+public final class MenuLayout {
+
+ public static void display(InventoryModule inventoryModule, Player viewer) {
+ // Live parts refresh automatically only while the Apollo config enables
+ // modules.inventory.buttons.live-broadcast (see the inventory module docs)
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> {
+ InventoryButton shop = InventoryButton.builder()
+ .id("shop")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(4, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("EMERALD")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Shop", NamedTextColor.GREEN),
+ Component.text("Browse categories and buy items", NamedTextColor.GRAY),
+ Component.text("Click to open", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/shop"))
+ .build();
+
+ InventoryButton spawn = InventoryButton.builder()
+ .id("spawn")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(48, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("RED_BED")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Spawn", NamedTextColor.AQUA),
+ Component.text("Teleport back to spawn", NamedTextColor.GRAY),
+ Component.text("Click to teleport", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/spawn"))
+ .build();
+
+ InventoryButton warps = InventoryButton.builder()
+ .id("warps")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(4, 48))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("COMPASS")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Warps", NamedTextColor.AQUA),
+ Component.text("Browse public warps", NamedTextColor.GRAY),
+ Component.text("Click to teleport", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/warps"))
+ .build();
+
+ InventoryButton enderChest = InventoryButton.builder()
+ .id("enderchest")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(48, 48))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("ENDER_CHEST")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Ender Chest", NamedTextColor.LIGHT_PURPLE),
+ Component.text("Open your personal storage", NamedTextColor.GRAY),
+ Component.text("Click to open", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/enderchest"))
+ .build();
+
+ InventoryButton balance = InventoryButton.builder()
+ .id("balance")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 92))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("GOLD_INGOT")
+ .build())
+ .append(ApolloButtonContentPart.live(apolloViewer -> Component.text("$" +
+ String.format("%,d", getBalance(apolloViewer.getUniqueId())), NamedTextColor.GOLD),
+ Duration.ofMillis(2500L)))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(Component.text("Your balance", NamedTextColor.GOLD)))
+ .build();
+
+ InventoryButton profile = InventoryButton.builder()
+ .id("profile")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(4, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.CIRCLE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3
+ .profile(Profile.builder()
+ .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"))
+ .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19")
+ .signature("")
+ .build())
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text(viewer.getName(), NamedTextColor.GOLD),
+ Component.text("View your stats", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/profile"))
+ .build();
+
+ InventoryButton settings = InventoryButton.builder()
+ .id("settings")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(48, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.CIRCLE)
+ .backgroundColor(new Color(222, 160, 60, 85))
+ .borderColor(new Color(255, 218, 150, 140))
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("COMPARATOR")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Settings", NamedTextColor.WHITE),
+ Component.text("Server preferences", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/settings"))
+ .build();
+
+ InventoryButton vote = InventoryButton.builder()
+ .id("vote")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 52))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(new Color(34, 204, 68, 64))
+ .borderColor(new Color(190, 255, 205, 110))
+ .hoveredBackgroundColor(new Color(34, 204, 68, 130))
+ .hoveredBorderColor(new Color(190, 255, 205, 210))
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Vote"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Vote", NamedTextColor.GREEN),
+ Component.text("Vote daily for rewards", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.openUrl("https://example.com/vote"))
+ .build();
+
+ InventoryButton discord = InventoryButton.builder()
+ .id("discord")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 84))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(new Color(88, 101, 242, 90))
+ .borderColor(new Color(150, 160, 250, 140))
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Discord"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Discord", NamedTextColor.BLUE),
+ Component.text("Join our community", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.openUrl("https://lunarclient.dev/discord"))
+ .build();
+
+ InventoryButton lobby = InventoryButton.builder()
+ .id("lobby")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 136))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Back to Lobby"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Back to Lobby", NamedTextColor.RED),
+ Component.text("Return to the main lobby", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/lobby"))
+ .build();
+
+ inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(shop, spawn, warps, enderChest, balance,
+ profile, settings, vote, discord, lobby));
+ });
+ }
+
+ // Demo economy: replace with your economy plugin lookup (e.g. Vault)
+ private static long getBalance(UUID playerIdentifier) {
+ long drift = (System.currentTimeMillis() / 10_000L) % 250L;
+ return 1_000L + Math.abs(playerIdentifier.hashCode() % 4_000) + drift;
+ }
+
+ private MenuLayout() {
+ }
+
+}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java
new file mode 100644
index 00000000..dc1633db
--- /dev/null
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/MinigameLayout.java
@@ -0,0 +1,244 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.example.api.module.inventorybuttons;
+
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.common.button.ApolloButtonShape;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.action.ApolloButtonAction;
+import com.lunarclient.apollo.common.button.action.ApolloButtonClientAction;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.common.icon.ItemStackIcon;
+import com.lunarclient.apollo.common.location.HudPosition;
+import com.lunarclient.apollo.common.profile.Profile;
+import com.lunarclient.apollo.mods.impl.ModMinimap;
+import com.lunarclient.apollo.mods.impl.ModWaypoints;
+import com.lunarclient.apollo.module.inventory.InventoryButton;
+import com.lunarclient.apollo.module.inventory.InventoryButtonBox;
+import com.lunarclient.apollo.module.inventory.InventoryModule;
+import com.lunarclient.apollo.module.inventory.InventoryType;
+import com.lunarclient.apollo.module.modsetting.ModSettingModule;
+import com.lunarclient.apollo.player.ApolloPlayer;
+import java.awt.Color;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.UUID;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.Statistic;
+import org.bukkit.entity.Player;
+
+public final class MinigameLayout {
+
+ public static void display(InventoryModule inventoryModule, Player viewer) {
+ // Live parts refresh automatically only while the Apollo config enables
+ // modules.inventory.buttons.live-broadcast (see the inventory module docs)
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> {
+ InventoryButton mapInfo = InventoryButton.builder()
+ .id("map-info")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 8))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Map: Apollo", NamedTextColor.AQUA))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Built by the Lunar Client Team"),
+ Component.text("Released in 2024", NamedTextColor.GRAY)))
+ .build();
+
+ InventoryButton kills = InventoryButton.builder()
+ .id("kills")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 40))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("IRON_SWORD")
+ .build())
+ .append(apolloViewer -> Component.text("Kills: ", NamedTextColor.GRAY)
+ .append(Component.text(getKills(apolloViewer), NamedTextColor.RED)),
+ Duration.ofMillis(2500L))
+ .scale(1.0F)
+ .build())
+ .build();
+
+ InventoryButton lobby = InventoryButton.builder()
+ .id("lobby")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 136))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .content(ApolloButtonContent.builder()
+ .append(Component.text("Back to Lobby"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Back to Lobby", NamedTextColor.RED),
+ Component.text("Return to the main lobby", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/lobby"))
+ .build();
+
+ InventoryButton profile = InventoryButton.builder()
+ .id("profile")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(4, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.CIRCLE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("PLAYER_HEAD") // use "skull" for legacy with customModelData set to 3
+ .profile(Profile.builder()
+ .id(UUID.fromString("f17627d8-1a97-487b-92ea-c04f413394bd"))
+ .texture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWQ4MjUwNWJjZjNiYTU5YzJiZTdlMmQzNmY0ZTJiZGE4MzZmMmZkMTk0YjYyMTJhMmExYzRiNGEyYTQ3MWUifX19")
+ .signature("")
+ .build())
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text(viewer.getName(), NamedTextColor.GOLD),
+ Component.text("View your stats", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/profile"))
+ .build();
+
+ InventoryButton settings = InventoryButton.builder()
+ .id("settings")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(48, 4))
+ .size(InventoryButton.SIZE_MEDIUM)
+ .shape(ApolloButtonShape.CIRCLE)
+ .backgroundColor(new Color(222, 160, 60, 85))
+ .borderColor(new Color(255, 218, 150, 140))
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("COMPARATOR")
+ .build())
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Settings", NamedTextColor.WHITE),
+ Component.text("Server preferences", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.runCommand("/settings"))
+ .build();
+
+ ModSettingModule modSettingModule = Apollo.getModuleManager().getModule(ModSettingModule.class);
+ boolean minimapEnabled = modSettingModule.getStatus(apolloPlayer, ModMinimap.ENABLED);
+ boolean waypointsEnabled = modSettingModule.getStatus(apolloPlayer, ModWaypoints.ENABLED);
+
+ InventoryButton.InventoryButtonBuilder, ?> showMapBuilder = InventoryButton.builder()
+ .id("show-map")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 52))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("FILLED_MAP")
+ .build())
+ .append(Component.text("Show Map"))
+ .scale(1.0F)
+ .build());
+
+ if (minimapEnabled) {
+ showMapBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Show Map", NamedTextColor.AQUA),
+ Component.text("Open the fullscreen minimap view", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_MINIMAP_VIEW));
+ } else {
+ showMapBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Minimap mod must be enabled", NamedTextColor.RED)));
+ }
+
+ InventoryButton showMap = showMapBuilder.build();
+
+ InventoryButton.InventoryButtonBuilder, ?> waypointsBuilder = InventoryButton.builder()
+ .id("waypoints")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 84))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("LODESTONE")
+ .build())
+ .append(Component.text("Waypoints"))
+ .scale(1.0F)
+ .build());
+
+ if (waypointsEnabled) {
+ waypointsBuilder
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .tooltip(ApolloButtonTooltip.of(
+ Component.text("Waypoints", NamedTextColor.GOLD),
+ Component.text("Manage your waypoints", NamedTextColor.GRAY)))
+ .onClick(ApolloButtonAction.clientAction(ApolloButtonClientAction.OPEN_WAYPOINTS_MENU));
+ } else {
+ waypointsBuilder
+ .backgroundColor(new Color(224, 64, 64, 128))
+ .borderColor(new Color(255, 200, 200, 140))
+ .tooltip(ApolloButtonTooltip.of(Component.text("Waypoints mod must be enabled", NamedTextColor.RED)));
+ }
+
+ InventoryButton waypoints = waypointsBuilder.build();
+
+ inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(mapInfo, kills, lobby,
+ profile, settings, showMap, waypoints));
+ });
+ }
+
+ private static int getKills(ApolloPlayer apolloViewer) {
+ Player player = Bukkit.getPlayer(apolloViewer.getUniqueId());
+ return player != null ? player.getStatistic(Statistic.PLAYER_KILLS) : 0;
+ }
+
+ private MinigameLayout() {
+ }
+
+}
diff --git a/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java
new file mode 100644
index 00000000..71922aca
--- /dev/null
+++ b/example/bukkit/api/src/main/java/com/lunarclient/apollo/example/api/module/inventorybuttons/StaffLayout.java
@@ -0,0 +1,327 @@
+/*
+ * This file is part of Apollo, licensed under the MIT License.
+ *
+ * Copyright (c) 2026 Moonsworth
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.lunarclient.apollo.example.api.module.inventorybuttons;
+
+import com.lunarclient.apollo.Apollo;
+import com.lunarclient.apollo.common.button.ApolloButtonShape;
+import com.lunarclient.apollo.common.button.ApolloButtonTooltip;
+import com.lunarclient.apollo.common.button.action.ApolloButtonAction;
+import com.lunarclient.apollo.common.button.content.ApolloButtonContent;
+import com.lunarclient.apollo.common.icon.ItemStackIcon;
+import com.lunarclient.apollo.common.location.HudPosition;
+import com.lunarclient.apollo.example.util.ServerStatsUtil;
+import com.lunarclient.apollo.module.inventory.InventoryButton;
+import com.lunarclient.apollo.module.inventory.InventoryButtonBox;
+import com.lunarclient.apollo.module.inventory.InventoryModule;
+import com.lunarclient.apollo.module.inventory.InventoryType;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public final class StaffLayout {
+
+ private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
+
+ public static void display(InventoryModule inventoryModule, Player viewer) {
+ // Live parts refresh automatically only while the Apollo config enables
+ // modules.inventory.buttons.live-broadcast (see the inventory module docs)
+ Apollo.getPlayerManager().getPlayer(viewer.getUniqueId()).ifPresent(apolloPlayer -> {
+ InventoryButton players = InventoryButton.builder()
+ .id("players")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 8))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(apolloViewer -> Component.text("Players: ", NamedTextColor.GRAY)
+ .append(Component.text(Bukkit.getOnlinePlayers().size(), NamedTextColor.GREEN)),
+ Duration.ofSeconds(1))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.live(apolloViewer -> Arrays.asList(
+ Component.text("Players currently online", NamedTextColor.GRAY),
+ Component.empty(), refreshedLine()),
+ Duration.ofSeconds(1)))
+ .build();
+
+ InventoryButton tps = InventoryButton.builder()
+ .id("tps")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 40))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(apolloViewer -> tpsContent(), Duration.ofSeconds(1))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.live(apolloViewer -> tpsTooltip(), Duration.ofSeconds(1)))
+ .build();
+
+ InventoryButton cpu = InventoryButton.builder()
+ .id("cpu")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 72))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(apolloViewer -> cpuContent(), Duration.ofSeconds(1))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.live(apolloViewer -> cpuTooltip(), Duration.ofSeconds(1)))
+ .build();
+
+ InventoryButton ram = InventoryButton.builder()
+ .id("ram")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.LEFT)
+ .position(HudPosition.of(6, 104))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(apolloViewer -> ramContent(), Duration.ofSeconds(1))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.live(apolloViewer -> ramTooltip(), Duration.ofSeconds(1)))
+ .build();
+
+ InventoryButton survival = InventoryButton.builder()
+ .id("survival")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 8))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("IRON_SWORD")
+ .build())
+ .append(Component.text("Survival"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/gamemode survival"))
+ .build();
+
+ InventoryButton creative = InventoryButton.builder()
+ .id("creative")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 40))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("GRASS_BLOCK")
+ .build())
+ .append(Component.text("Creative"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/gamemode creative"))
+ .build();
+
+ InventoryButton adventure = InventoryButton.builder()
+ .id("adventure")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 72))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("FILLED_MAP")
+ .build())
+ .append(Component.text("Adventure"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/gamemode adventure"))
+ .build();
+
+ InventoryButton spectator = InventoryButton.builder()
+ .id("spectator")
+ .inventoryType(InventoryType.PLAYER)
+ .box(InventoryButtonBox.RIGHT)
+ .position(HudPosition.of(6, 104))
+ .size(InventoryButton.SIZE_WIDE)
+ .shape(ApolloButtonShape.ROUNDED_SQUARE)
+ .backgroundColor(InventoryButton.DEFAULT_BACKGROUND_COLOR)
+ .borderColor(InventoryButton.DEFAULT_BORDER_COLOR)
+ .content(ApolloButtonContent.builder()
+ .append(ItemStackIcon.builder()
+ .itemName("ENDER_EYE")
+ .build())
+ .append(Component.text("Spectator"))
+ .scale(1.0F)
+ .build())
+ .tooltip(ApolloButtonTooltip.of(Component.text("Click to switch!", NamedTextColor.YELLOW)))
+ .onClick(ApolloButtonAction.runCommand("/gamemode spectator"))
+ .build();
+
+ inventoryModule.displayInventoryButtons(apolloPlayer, Arrays.asList(players, tps, cpu, ram,
+ survival, creative, adventure, spectator));
+ });
+ }
+
+ private static Component tpsContent() {
+ double[] tps = ServerStatsUtil.getTps();
+ if (tps == null || tps.length == 0) {
+ return Component.text("TPS: N/A", NamedTextColor.GRAY);
+ }
+
+ double recent = Math.min(20.0D, tps[0]);
+ return Component.text("TPS: ", NamedTextColor.GRAY)
+ .append(Component.text(String.format("%.1f", recent), tpsColor(recent)));
+ }
+
+ private static List tpsTooltip() {
+ double[] tps = ServerStatsUtil.getTps();
+ if (tps == null || tps.length < 3) {
+ return Arrays.asList(
+ Component.text("TPS averages require a Paper based server", NamedTextColor.GRAY),
+ Component.empty(),
+ refreshedLine());
+ }
+
+ return Arrays.asList(
+ tpsAverageLine("1m", tps[0]),
+ tpsAverageLine("5m", tps[1]),
+ tpsAverageLine("15m", tps[2]),
+ Component.empty(),
+ refreshedLine());
+ }
+
+ private static Component tpsAverageLine(String window, double average) {
+ double tps = Math.min(20.0D, average);
+ return Component.text(window + ": ", NamedTextColor.GRAY)
+ .append(Component.text(String.format("%.2f", tps), tpsColor(tps)));
+ }
+
+ private static NamedTextColor tpsColor(double tps) {
+ if (tps >= 18.0D) {
+ return NamedTextColor.GREEN;
+ }
+
+ return tps >= 15.0D ? NamedTextColor.YELLOW : NamedTextColor.RED;
+ }
+
+ private static Component cpuContent() {
+ double load = ServerStatsUtil.getSystemLoadAverage();
+ if (load < 0.0D) {
+ return Component.text("CPU: N/A", NamedTextColor.GRAY);
+ }
+
+ return Component.text("CPU: ", NamedTextColor.GRAY)
+ .append(Component.text(String.format("%.2f", load), cpuColor(load)));
+ }
+
+ private static List cpuTooltip() {
+ double load = ServerStatsUtil.getSystemLoadAverage();
+ if (load < 0.0D) {
+ return Arrays.asList(
+ Component.text("The system load average is unavailable", NamedTextColor.GRAY),
+ Component.empty(),
+ refreshedLine());
+ }
+
+ int cores = ServerStatsUtil.getAvailableProcessors();
+ double perCore = load * 100.0D / cores;
+ return Arrays.asList(
+ Component.text("System load average (last minute)", NamedTextColor.GRAY),
+ Component.text("Cores: ", NamedTextColor.GRAY)
+ .append(Component.text(cores, NamedTextColor.WHITE)),
+ Component.text("Per core: ", NamedTextColor.GRAY)
+ .append(Component.text(String.format("%.0f%%", perCore), cpuColor(load))),
+ Component.empty(),
+ refreshedLine());
+ }
+
+ private static NamedTextColor cpuColor(double load) {
+ double perCore = load / ServerStatsUtil.getAvailableProcessors();
+ if (perCore < 0.5D) {
+ return NamedTextColor.GREEN;
+ }
+
+ return perCore < 1.0D ? NamedTextColor.YELLOW : NamedTextColor.RED;
+ }
+
+ private static Component ramContent() {
+ long used = ServerStatsUtil.getUsedRamMb();
+ long max = ServerStatsUtil.getMaxRamMb();
+ long percent = max <= 0 ? 0 : used * 100 / max;
+
+ return Component.text("RAM: ", NamedTextColor.GRAY)
+ .append(Component.text(percent + "%", ramColor(percent)));
+ }
+
+ private static List ramTooltip() {
+ return Arrays.asList(
+ Component.text("Used: ", NamedTextColor.GRAY)
+ .append(Component.text(ServerStatsUtil.getUsedRamMb() + " MB", NamedTextColor.WHITE)),
+ Component.text("Max: ", NamedTextColor.GRAY)
+ .append(Component.text(ServerStatsUtil.getMaxRamMb() + " MB", NamedTextColor.WHITE)),
+ Component.empty(),
+ refreshedLine());
+ }
+
+ private static NamedTextColor ramColor(long percent) {
+ if (percent < 60) {
+ return NamedTextColor.GREEN;
+ }
+
+ return percent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED;
+ }
+
+ private static Component refreshedLine() {
+ return Component.text("Updated at " + LocalTime.now().format(TIME_FORMAT), NamedTextColor.YELLOW, TextDecoration.ITALIC);
+ }
+
+ private StaffLayout() {
+ }
+
+}
diff --git a/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java b/example/bukkit/common/src/main/java/com/lunarclient/apollo/example/ApolloExamplePlugin.java
index 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/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() {
}
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())