Add Advancement API beta

This commit is contained in:
NotAlexNoyle committed 2026-05-18 08:46:40 -04:00
1 parent e5d9a8663c
commit 24b385d3f7
10 files changed
+666 -24

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
[submodule "libs/Utilities-OG"]
path = libs/Utilities-OG
url = https://github.com/true-og/Utilities-OG
+87
View File
@@ -15,6 +15,93 @@ See [Template-OG](https://github.com/true-og/Template-OG) or [KotlinTemplate-OG]
[SudoSigns-OG](https://github.com/true-og/sudosigns-og)
[TheHerobrine-OG](https://github.com/true-og/TheHerobrine-OG/)
## Core GUI API
Extend `GUIBase`, add `GUIItem`s in `setupItems()`, and call `open(false)`.
Java:
```java
public final class ExampleMenu extends GUIBase {
public ExampleMenu(JavaPlugin plugin, Player player) {
super(plugin, player, "&6Example Menu", 27, true);
}
@Override
public void setupItems() {
GUIItem item = new GUIItem(Material.DIAMOND, 1, "&bReward");
item.lore(List.of(UtilitiesOG.trueogExpand("&7Click to claim.")));
item.button(new GUIButton() {
@Override public boolean leftClick() { getPlayer().sendMessage("Claimed"); return true; }
@Override public boolean leftClickShift() { return leftClick(); }
@Override public boolean rightClick() { return leftClick(); }
@Override public boolean rightClickShift() { return leftClick(); }
});
addItem(13, item);
}
}
new ExampleMenu(plugin, player).open(false);
```
Kotlin:
```kotlin
class ExampleMenu(plugin: JavaPlugin, player: Player) : GUIBase(plugin, player, "&6Example Menu", 27, true) {
override fun setupItems() {
val item = GUIItem(Material.DIAMOND, 1, "&bReward")
item.lore(listOf(UtilitiesOG.trueogExpand("&7Click to claim.")))
item.button(object : GUIButton {
override fun leftClick(): Boolean { player.sendMessage("Claimed"); return true }
override fun leftClickShift() = leftClick()
override fun rightClick() = leftClick()
override fun rightClickShift() = leftClick()
})
addItem(13, item)
}
}
ExampleMenu(plugin, player).open(false)
```
Notes:
* `fillEmpty = true` fills unused slots with gray glass panes.
* GUI item clicks are cancelled automatically.
* A `GUIButton` method should return `true` when the click was handled.
* Non-button items can call `item.playErrorSound(false)` to make clicks silent.
## Advancement API
`AdvancementMenu` displays an ordered list of read-only advancement items in a paginated inventory. Items are placed
from the top-left slot downward through each column, reserving the bottom row for previous, close, and next controls.
Java:
```java
Map<String, List<String>> values = new LinkedHashMap<>();
values.put("&cHome Two", List.of("&7Unlock the second home.", "&eRequired: &fWin 1 duel"));
values.put("&cHome Three", List.of("&7Unlock the third home.", "&eRequired: &fBuild a full beacon"));
AdvancementMenu.open(plugin, player, "&6Advancements", values);
```
Kotlin:
```kotlin
val values = linkedMapOf(
"&cHome Two" to listOf("&7Unlock the second home.", "&eRequired: &fWin 1 duel"),
"&cHome Three" to listOf("&7Unlock the third home.", "&eRequired: &fBuild a full beacon"),
)
AdvancementMenu.open(plugin, player, "&6Advancements", values)
```
The simple map API displays every entry as `RED_WOOL`. Use an ordered map, such as `LinkedHashMap`, when item order
matters. Callers that need custom state or icons can still pass `List<AdvancementItem>` directly.
## License
Public domain.
-1
View File
@@ -56,7 +56,6 @@ repositories {
dependencies {
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
compileOnly("io.github.miniplaceholders:miniplaceholders-api:2.2.3") // Import MiniPlaceholders API.
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
}
/* ---------------------- Reproducible jars ---------------------------- */
+10 -17
View File
@@ -5,7 +5,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Sound;
@@ -28,7 +27,6 @@ import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@@ -162,17 +160,15 @@ public abstract class GUIBase implements Listener {
@Override
public void run() {
final Inventory inventory = ((HumanEntity) player).getOpenInventory().getTopInventory();
final Component inventoryTitle = player.getOpenInventory().title();
final String inventoryTitleText = inventoryTitle.examinableName();
if (!StringUtils.equalsIgnoreCase(inventoryTitleText, inventoryName)) {
final Inventory openInventory = ((HumanEntity) player).getOpenInventory().getTopInventory();
if (openInventory != inventory) {
cancel();
return;
}
inventory.clear();
openInventory.clear();
inventoryContents.clear();
setupItems();
int counter = 0;
@@ -194,18 +190,18 @@ public abstract class GUIBase implements Listener {
itemErrors.remove(i.getKey());
itemTimings.remove(i.getKey());
inventory.setItem(i.getKey(), i.getValue().build());
openInventory.setItem(i.getKey(), i.getValue().build());
continue;
}
inventory.setItem(i.getKey(), error.build());
openInventory.setItem(i.getKey(), error.build());
continue;
}
inventory.setItem(i.getKey(), i.getValue().build());
openInventory.setItem(i.getKey(), i.getValue().build());
}
@@ -213,9 +209,9 @@ public abstract class GUIBase implements Listener {
for (int i = 0; i < inventorySize; i++) {
if (inventory.getItem(i) == null) {
if (openInventory.getItem(i) == null) {
inventory.setItem(i, fillSlot());
openInventory.setItem(i, fillSlot());
}
@@ -368,10 +364,7 @@ public abstract class GUIBase implements Listener {
public void close(InventoryCloseEvent event) {
final Player closer = (Player) event.getPlayer();
final Component inventoryTitle = player.getOpenInventory().title();
final String inventoryTitleText = inventoryTitle.examinableName();
final boolean condition = StringUtils.equalsIgnoreCase(inventoryTitleText, inventoryName) && isOpen
&& closer == player;
final boolean condition = event.getInventory() == inventory && isOpen && closer == player;
if (!condition) {
return;
@@ -429,4 +422,4 @@ public abstract class GUIBase implements Listener {
}
}
}
+3 -2
View File
@@ -16,7 +16,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import net.kyori.adventure.text.TextComponent;
import net.trueog.utilitiesog.UtilitiesOG;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@Accessors(fluent = true)
public class GUIItem {
@@ -149,7 +149,8 @@ public class GUIItem {
public ItemStack build() {
final ItemStack i;
final TextComponent displayNameTextComponent = UtilitiesOG.trueogExpand(displayName);
final TextComponent displayNameTextComponent = LegacyComponentSerializer.legacyAmpersand()
.deserialize(displayName);
final List<TextComponent> loreTextComponent = lore;
try {
@@ -0,0 +1,64 @@
package net.trueog.gxui.advancements;
import java.util.Objects;
// One requirement entry beneath an advancement. Built by AdvancementMenu.Builder.
final class AdvancementAttribute {
private final String name;
private final boolean done;
private final long current;
private final long target;
private final boolean hasProgress;
AdvancementAttribute(String name, boolean done) {
this.name = Objects.requireNonNull(name, "name");
this.done = done;
this.current = 0L;
this.target = 0L;
this.hasProgress = false;
}
AdvancementAttribute(String name, long current, long target) {
this.name = Objects.requireNonNull(name, "name");
this.current = current;
this.target = target;
this.done = current >= target;
this.hasProgress = true;
}
String getName() {
return name;
}
boolean isDone() {
return done;
}
boolean hasProgress() {
return hasProgress;
}
long getCurrent() {
return current;
}
long getTarget() {
return target;
}
}
@@ -0,0 +1,43 @@
package net.trueog.gxui.advancements;
import java.util.List;
import java.util.Objects;
record AdvancementItem(String name, AdvancementState state, List<AdvancementAttribute> attributes,
List<String> footer)
{
AdvancementItem(String name, AdvancementState state, List<AdvancementAttribute> attributes, List<String> footer) {
this.name = Objects.requireNonNull(name, "name");
this.state = Objects.requireNonNull(state, "state");
this.attributes = List.copyOf(Objects.requireNonNull(attributes, "attributes"));
this.footer = List.copyOf(Objects.requireNonNull(footer, "footer"));
}
String getName() {
return name;
}
AdvancementState getState() {
return state;
}
List<AdvancementAttribute> getAttributes() {
return attributes;
}
List<String> getFooter() {
return footer;
}
}
@@ -0,0 +1,418 @@
package net.trueog.gxui.advancements;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.trueog.gxui.GUIBase;
import net.trueog.gxui.GUIButton;
import net.trueog.gxui.GUIItem;
// Read-only paginated inventory for advancement-style progress displays.
// Construct with AdvancementMenu.builder(plugin, player, title). Run open() to display.
public class AdvancementMenu extends GUIBase {
public static final int INVENTORY_SIZE = 54;
public static final int ADVANCEMENTS_PER_PAGE = 45;
public static final int PREVIOUS_PAGE_SLOT = 45;
public static final int CLOSE_SLOT = 49;
public static final int NEXT_PAGE_SLOT = 53;
private static final List<Integer> ADVANCEMENT_SLOTS = createAdvancementSlots();
private final JavaPlugin plugin;
private final String baseTitle;
private final List<AdvancementItem> advancementItems;
private final int page;
private AdvancementMenu(JavaPlugin plugin, Player player, String inventoryName,
List<AdvancementItem> advancementItems, int page)
{
super(plugin, player, buildInventoryName(inventoryName, advancementItems, page), INVENTORY_SIZE, true);
this.plugin = plugin;
this.baseTitle = inventoryName;
this.advancementItems = advancementItems;
this.page = normalizePage(page, getTotalPages(advancementItems.size()));
}
public static Builder builder(JavaPlugin plugin, Player player, String inventoryName) {
return new Builder(plugin, player, inventoryName);
}
public int getPage() {
return page;
}
public int getTotalPages() {
return getTotalPages(advancementItems.size());
}
@Override
public void setupItems() {
final int firstAdvancement = page * ADVANCEMENTS_PER_PAGE;
final int lastAdvancement = Math.min(firstAdvancement + ADVANCEMENTS_PER_PAGE, advancementItems.size());
for (int advancementIndex = firstAdvancement; advancementIndex < lastAdvancement; advancementIndex++) {
final int pageIndex = advancementIndex - firstAdvancement;
addItem(ADVANCEMENT_SLOTS.get(pageIndex), buildAdvancementItem(advancementItems.get(advancementIndex)));
}
if (page > 0) {
addItem(PREVIOUS_PAGE_SLOT, buildPageButton(Material.ARROW, "&ePrevious Page", page - 1));
}
addItem(CLOSE_SLOT, buildCloseButton());
if (page + 1 < getTotalPages()) {
addItem(NEXT_PAGE_SLOT, buildPageButton(Material.ARROW, "&eNext Page", page + 1));
}
}
private GUIItem buildAdvancementItem(AdvancementItem advancementItem) {
final AdvancementState state = advancementItem.getState();
final String displayName = state.getColor() + advancementItem.getName();
final GUIItem guiItem = new GUIItem(state.getDefaultMaterial(), 1, displayName);
guiItem.lore(toLoreComponents(buildLore(advancementItem)));
guiItem.playErrorSound(false);
return guiItem;
}
private static List<String> buildLore(AdvancementItem advancementItem) {
final List<String> lore = new ArrayList<>();
lore.add(advancementItem.getState().getStatusLine());
final List<AdvancementAttribute> attributes = advancementItem.getAttributes();
if (!attributes.isEmpty()) {
lore.add("");
lore.add("&7Requirements:");
attributes.forEach(attribute -> lore.add(formatAttribute(attribute)));
}
final List<String> footer = advancementItem.getFooter();
if (!footer.isEmpty()) {
lore.add("");
lore.addAll(footer);
}
return lore;
}
private static String formatAttribute(AdvancementAttribute attribute) {
final String color = attribute.isDone() ? "&a" : "&c";
if (attribute.hasProgress()) {
return color + attribute.getName() + ": &f" + attribute.getCurrent() + "&7/&f" + attribute.getTarget();
}
final String status = attribute.isDone() ? "Done" : "Not done";
return color + attribute.getName() + ": &f" + status;
}
private GUIItem buildCloseButton() {
final GUIItem guiItem = new GUIItem(Material.BARRIER, 1, "&cClose");
guiItem.playErrorSound(false);
guiItem.button(createButton(() -> getPlayer().closeInventory()));
return guiItem;
}
private GUIItem buildPageButton(Material material, String displayName, int targetPage) {
final GUIItem guiItem = new GUIItem(material, 1, displayName);
guiItem.lore(toLoreComponents(List.of("&7Page " + (targetPage + 1) + " of " + getTotalPages())));
guiItem.playErrorSound(false);
guiItem.button(createButton(() -> openPage(targetPage)));
return guiItem;
}
private void openPage(int targetPage) {
new AdvancementMenu(plugin, getPlayer(), baseTitle, advancementItems, targetPage).open(false);
}
private GUIButton createButton(Runnable clickHandler) {
return new GUIButton() {
@Override
public boolean leftClick() {
return handleClick();
}
@Override
public boolean leftClickShift() {
return handleClick();
}
@Override
public boolean rightClick() {
return handleClick();
}
@Override
public boolean rightClickShift() {
return handleClick();
}
private boolean handleClick() {
clickHandler.run();
return true;
}
};
}
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacyAmpersand();
private List<TextComponent> toLoreComponents(List<String> lore) {
return lore.stream().map(AdvancementMenu::toTextComponent).toList();
}
private static TextComponent toTextComponent(String line) {
final Component component = LEGACY.deserialize(line);
// LegacyComponentSerializer always produces a TextComponent root.
return (TextComponent) component;
}
private static String buildInventoryName(String inventoryName, List<AdvancementItem> advancementItems, int page) {
Objects.requireNonNull(inventoryName, "inventoryName");
final int totalPages = getTotalPages(Objects.requireNonNull(advancementItems, "advancementItems").size());
if (totalPages == 1) {
return inventoryName;
}
final int normalizedPage = normalizePage(page, totalPages);
return inventoryName + " &7(" + (normalizedPage + 1) + "/" + totalPages + ")";
}
private static int getTotalPages(int itemCount) {
return Math.max(1, (int) Math.ceil((double) itemCount / ADVANCEMENTS_PER_PAGE));
}
private static int normalizePage(int page, int totalPages) {
if (page < 0) {
return 0;
}
return page >= totalPages ? totalPages - 1 : page;
}
private static List<Integer> createAdvancementSlots() {
final List<Integer> slots = new ArrayList<>(ADVANCEMENTS_PER_PAGE);
// Column-first ordering matches "start top-left and go down".
for (int column = 0; column < 9; column++) {
for (int row = 0; row < 5; row++) {
slots.add(row * 9 + column);
}
}
return Collections.unmodifiableList(slots);
}
private static int parsePage(String[] args) {
if (args == null || args.length == 0) {
return 0;
}
try {
return Math.max(0, Integer.parseInt(args[0]) - 1);
} catch (NumberFormatException ignored) {
return 0;
}
}
// Fluent builder for an AdvancementMenu. Plugins only depend on this class,
// AdvancementState, and Java primitives.
public static final class Builder {
private final JavaPlugin plugin;
private final Player player;
private final String title;
private final List<AdvancementItem> items = new ArrayList<>();
private String pendingName;
private AdvancementState pendingState;
private List<AdvancementAttribute> pendingAttributes;
private List<String> pendingFooter;
private Builder(JavaPlugin plugin, Player player, String title) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
this.player = Objects.requireNonNull(player, "player");
this.title = Objects.requireNonNull(title, "title");
}
// Start a new advancement slot. Following attribute/footer calls attach to this
// one.
public Builder add(String name, AdvancementState state) {
flushPending();
this.pendingName = Objects.requireNonNull(name, "name");
this.pendingState = Objects.requireNonNull(state, "state");
this.pendingAttributes = new ArrayList<>();
this.pendingFooter = new ArrayList<>();
return this;
}
// Add a progress-counter attribute (e.g. 250/1000). Coloured by current >=
// target.
public Builder progress(String name, long current, long target) {
requirePending();
pendingAttributes.add(new AdvancementAttribute(name, current, target));
return this;
}
// Add a boolean attribute.
public Builder bool(String name, boolean done) {
requirePending();
pendingAttributes.add(new AdvancementAttribute(name, done));
return this;
}
// Append free-form footer lines (formatting allowed).
public Builder footer(String... lines) {
requirePending();
Collections.addAll(pendingFooter, lines);
return this;
}
// Open at the page parsed from command args (e.g. "/cmd 2").
public AdvancementMenu open(String[] args) {
return openAt(parsePage(args));
}
// Open at page 1.
public AdvancementMenu open() {
return openAt(0);
}
private AdvancementMenu openAt(int page) {
flushPending();
final AdvancementMenu menu = new AdvancementMenu(plugin, player, title, List.copyOf(items), page);
menu.open(false);
return menu;
}
private void requirePending() {
if (pendingName == null) {
throw new IllegalStateException("Call add(name, state) before adding attributes or footer lines");
}
}
private void flushPending() {
if (pendingName == null) {
return;
}
items.add(new AdvancementItem(pendingName, pendingState, pendingAttributes, pendingFooter));
pendingName = null;
pendingState = null;
pendingAttributes = null;
pendingFooter = null;
}
}
}
@@ -0,0 +1,41 @@
package net.trueog.gxui.advancements;
import org.bukkit.Material;
// Visual state of an advancement slot. Drives material colour and lore status line.
public enum AdvancementState {
COMPLETE(Material.LIME_WOOL, "&a", "&aCompleted"), IN_PROGRESS(Material.YELLOW_WOOL, "&e", "&eIn Progress"),
LOCKED(Material.GRAY_WOOL, "&7", "&7Locked");
private final Material defaultMaterial;
private final String color;
private final String statusLine;
AdvancementState(Material defaultMaterial, String color, String statusLine) {
this.defaultMaterial = defaultMaterial;
this.color = color;
this.statusLine = statusLine;
}
public Material getDefaultMaterial() {
return defaultMaterial;
}
String getColor() {
return color;
}
String getStatusLine() {
return statusLine;
}
}