From e61ab0d259f45fe38caf835ba25c47ffd404a807 Mon Sep 17 00:00:00 2001 From: sun-dev Date: Tue, 18 Aug 2026 13:25:58 +0530 Subject: [PATCH] improvements to menu section, new methods to Promise and brand-new built-in ConfirmMenu to simplify writing confirm menu --- .../dev/oum/oumlib/inventory/ChestMenu.java | 44 +++ .../dev/oum/oumlib/inventory/ConfirmMenu.java | 266 ++++++++++++++++++ .../dev/oum/oumlib/inventory/ItemBuilder.java | 8 +- .../java/dev/oum/oumlib/inventory/Layout.java | 13 +- .../java/dev/oum/oumlib/inventory/Menu.java | 2 +- .../main/java/dev/oum/oumlib/pdc/DataKey.java | 2 +- .../dev/oum/oumlib/scheduler/Promise.java | 28 +- .../dev/oum/oumlib/text/Localization.java | 2 - .../dev/oum/oumlib/text/Placeholders.java | 20 +- 9 files changed, 365 insertions(+), 20 deletions(-) create mode 100644 oumlib-core/src/main/java/dev/oum/oumlib/inventory/ConfirmMenu.java diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java index 70b9291..50d59bc 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java @@ -397,6 +397,50 @@ public static final class Builder { return this; } + @CheckReturnValue + public @NonNull Builder bindBorders(@Nullable ItemStack item, char... ignoredKeys) { + if (layout != null) { + Set ignored = new HashSet<>(); + for (char c : ignoredKeys) { + ignored.add(c); + } + for (char c : layout.characters()) { + if (!ignored.contains(c)) { + layout.bind(c, item); + } + } + } + return this; + } + + @CheckReturnValue + public @NonNull Builder bindBorders(@NonNull Supplier<@Nullable ItemStack> supplier, char... ignoredKeys) { + if (layout != null) { + Set ignored = new HashSet<>(); + for (char c : ignoredKeys) { + ignored.add(c); + } + for (char c : layout.characters()) { + if (!ignored.contains(c)) { + layout.bind(c, supplier); + } + } + } + return this; + } + + @CheckReturnValue + public @NonNull Builder fill(@Nullable ItemStack item) { + int totalSlots = rows * 9; + for (int i = 0; i < totalSlots; i++) { + final int slot = i; + if (!slotItems.containsKey(slot)) { + slotItems.put(slot, player -> item); + } + } + return this; + } + @CheckReturnValue public @NonNull Builder item(int slot, @Nullable ItemStack item) { this.slotItems.put(slot, player -> item); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ConfirmMenu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ConfirmMenu.java new file mode 100644 index 0000000..fc7c915 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ConfirmMenu.java @@ -0,0 +1,266 @@ +package dev.oum.oumlib.inventory; + +import dev.oum.oumlib.scheduler.Scheduler; +import net.kyori.adventure.sound.Sound; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public final class ConfirmMenu implements Menu { + + private final ChestMenu chestMenu; + + private ConfirmMenu(@NonNull Builder builder) { + ChestMenu.Builder cb = ChestMenu.builder() + .title(builder.title) + .rows(builder.rows); + + if (builder.pattern != null && builder.pattern.length > 0) { + cb.pattern(builder.pattern); + } else { + cb.pattern( + "#########", + " C D ", + "#########" + ); + } + + if (builder.openSound != null) cb.openSound(builder.openSound); + if (builder.closeSound != null) cb.closeSound(builder.closeSound); + if (builder.clickSound != null) cb.clickSound(builder.clickSound); + + if (builder.borderItem != null) { + cb.bindBorders(builder.borderItem, builder.confirmSlotChar, builder.denySlotChar); + } + + cb.bind(builder.confirmSlotChar, builder.confirmItem); + cb.onClick(builder.confirmSlotChar, ctx -> { + Player p = ctx.player(); + p.closeInventory(); + if (builder.confirmSound != null) p.playSound(builder.confirmSound); + if (builder.onConfirm != null) { + Scheduler.runFor(p, () -> builder.onConfirm.accept(p)); + } + }); + + cb.bind(builder.denySlotChar, builder.denyItem); + cb.onClick(builder.denySlotChar, ctx -> { + Player p = ctx.player(); + p.closeInventory(); + if (builder.denySound != null) p.playSound(builder.denySound); + if (builder.onDeny != null) { + Scheduler.runFor(p, () -> builder.onDeny.accept(p)); + } + }); + + if (builder.onClose != null) { + cb.onClose(builder.onClose); + } + + this.chestMenu = cb.build(); + } + + @Contract(" -> new") + @CheckReturnValue + public static @NonNull Builder builder() { + return new Builder(); + } + + @Override + public void open(@NonNull Player player) { + chestMenu.open(player); + } + + @Override + public void close(@NonNull Player player) { + chestMenu.close(player); + } + + @Override + public void closeAll() { + chestMenu.closeAll(); + } + + public static final class Builder { + private String title = "Are you sure?"; + private int rows = 3; + private String[] pattern; + private char confirmSlotChar = 'C'; + private char denySlotChar = 'D'; + + private Function confirmItem = p -> ItemBuilder.of(Material.LIME_CONCRETE) + .name("Confirm") + .build(); + private Function denyItem = p -> ItemBuilder.of(Material.RED_CONCRETE) + .name("Cancel") + .build(); + private ItemStack borderItem = ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build(); + + private Consumer onConfirm; + private Consumer onDeny; + private Consumer onClose; + + private Sound confirmSound; + private Sound denySound; + private Sound openSound; + private Sound closeSound; + private Sound clickSound; + + @CheckReturnValue + public @NonNull Builder title(@NonNull String title) { + this.title = title; + return this; + } + + @CheckReturnValue + public @NonNull Builder rows(int rows) { + if (rows < 1 || rows > 6) throw new IllegalArgumentException("Rows must be 1–6."); + this.rows = rows; + return this; + } + + @CheckReturnValue + public @NonNull Builder pattern(String @NonNull ... rows) { + this.pattern = rows; + return this; + } + + @CheckReturnValue + public @NonNull Builder pattern(@NonNull List rows) { + this.pattern = rows.toArray(new String[0]); + return this; + } + + @CheckReturnValue + public @NonNull Builder confirmSlot(char key) { + this.confirmSlotChar = key; + return this; + } + + @CheckReturnValue + public @NonNull Builder denySlot(char key) { + this.denySlotChar = key; + return this; + } + + @CheckReturnValue + public @NonNull Builder confirmItem(@Nullable ItemStack item) { + this.confirmItem = p -> item; + return this; + } + + @CheckReturnValue + public @NonNull Builder confirmItem(@NonNull Supplier<@Nullable ItemStack> supplier) { + this.confirmItem = p -> supplier.get(); + return this; + } + + @CheckReturnValue + public @NonNull Builder confirmItem(@NonNull Function<@NonNull Player, @Nullable ItemStack> function) { + this.confirmItem = function; + return this; + } + + @CheckReturnValue + public @NonNull Builder denyItem(@Nullable ItemStack item) { + this.denyItem = p -> item; + return this; + } + + @CheckReturnValue + public @NonNull Builder denyItem(@NonNull Supplier<@Nullable ItemStack> supplier) { + this.denyItem = p -> supplier.get(); + return this; + } + + @CheckReturnValue + public @NonNull Builder denyItem(@NonNull Function<@NonNull Player, @Nullable ItemStack> function) { + this.denyItem = function; + return this; + } + + @CheckReturnValue + public @NonNull Builder border(@Nullable ItemStack item) { + this.borderItem = item; + return this; + } + + @CheckReturnValue + public @NonNull Builder onConfirm(@Nullable Consumer onConfirm) { + this.onConfirm = onConfirm; + return this; + } + + @CheckReturnValue + public @NonNull Builder onConfirm(@Nullable Runnable onConfirm) { + this.onConfirm = onConfirm != null ? p -> onConfirm.run() : null; + return this; + } + + @CheckReturnValue + public @NonNull Builder onDeny(@Nullable Consumer onDeny) { + this.onDeny = onDeny; + return this; + } + + @CheckReturnValue + public @NonNull Builder onDeny(@Nullable Runnable onDeny) { + this.onDeny = onDeny != null ? p -> onDeny.run() : null; + return this; + } + + @CheckReturnValue + public @NonNull Builder onClose(@Nullable Consumer onClose) { + this.onClose = onClose; + return this; + } + + @CheckReturnValue + public @NonNull Builder confirmSound(@Nullable Sound sound) { + this.confirmSound = sound; + return this; + } + + @CheckReturnValue + public @NonNull Builder denySound(@Nullable Sound sound) { + this.denySound = sound; + return this; + } + + @CheckReturnValue + public @NonNull Builder openSound(@Nullable Sound sound) { + this.openSound = sound; + return this; + } + + @CheckReturnValue + public @NonNull Builder closeSound(@Nullable Sound sound) { + this.closeSound = sound; + return this; + } + + @CheckReturnValue + public @NonNull Builder clickSound(@Nullable Sound sound) { + this.clickSound = sound; + return this; + } + + @Contract(" -> new") + public @NonNull ConfirmMenu build() { + return new ConfirmMenu(this); + } + + public void open(@NonNull Player player) { + build().open(player); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java index 432bd3b..76e985a 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java @@ -66,11 +66,17 @@ private ItemBuilder(@NonNull ItemStack item) { @Contract("_ -> new") @CheckReturnValue public static @NonNull ItemBuilder from(@NonNull String identifier) { + return from(identifier, Material.STONE); + } + + @Contract("_, _ -> new") + @CheckReturnValue + public static @NonNull ItemBuilder from(@NonNull String identifier, @NonNull Material fallback) { return ItemBridge.getItem(identifier) .map(ItemBuilder::of) .orElseGet(() -> { Material mat = Material.matchMaterial(identifier); - return of(mat != null ? mat : Material.STONE); + return of(mat != null ? mat : fallback); }); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Layout.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Layout.java index e18ea3e..17dd6b1 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Layout.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Layout.java @@ -6,10 +6,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.function.Supplier; @@ -40,6 +37,14 @@ public Layout bind(char key, @NonNull Function<@NonNull Player, @Nullable ItemSt return this; } + public @NonNull String[] pattern() { + return pattern; + } + + public @NonNull Set characters() { + return slotMap.keySet(); + } + public List slotsFor(char key) { return slotMap.getOrDefault(key, List.of()); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Menu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Menu.java index 1952ca9..d57b0cb 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Menu.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/Menu.java @@ -2,7 +2,7 @@ import org.bukkit.entity.Player; -public sealed interface Menu permits ChestMenu, PaginatedMenu, AnvilMenu { +public sealed interface Menu permits ChestMenu, PaginatedMenu, AnvilMenu, ConfirmMenu { void open(Player player); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java index 5d93046..9f4410f 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java @@ -116,7 +116,6 @@ public record DataKey( return new Location(w, x, y, z, yaw, pitch); } }; - private static final Gson GSON = new Gson(); public static final PersistentDataType> STRING_LIST_TYPE = new PersistentDataType<>() { @Override public @NonNull Class getPrimitiveType() { @@ -140,6 +139,7 @@ public record DataKey( return GSON.fromJson(primitive, List.class); } }; + private static final Gson GSON = new Gson(); public static @NonNull DataKey string(@NonNull NamespacedKey key) { return new DataKey<>(key, PersistentDataType.STRING); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java index cad86fa..3aaf734 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java @@ -5,6 +5,7 @@ import org.jspecify.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -23,6 +24,20 @@ private Promise(CompletableFuture future) { return new Promise<>(future); } + public static @NonNull Promise completed(@Nullable U value) { + return new Promise<>(CompletableFuture.completedFuture(value)); + } + + public static @NonNull Promise empty() { + return new Promise<>(CompletableFuture.completedFuture(null)); + } + + public static @NonNull Promise failed(@NonNull Throwable throwable) { + CompletableFuture fut = new CompletableFuture<>(); + fut.completeExceptionally(throwable); + return new Promise<>(fut); + } + @Contract("_ -> new") public static @NonNull Promise supplyAsync(@NonNull Supplier supplier) { CompletableFuture fut = new CompletableFuture<>(); @@ -74,7 +89,7 @@ private Promise(CompletableFuture future) { return new Promise<>(fut); } - public static @NonNull Promise> all(@NonNull List> promises) { + public static @NonNull Promise> all(@NonNull Collection> promises) { CompletableFuture[] futures = promises.stream() .map(Promise::toCompletableFuture) .toArray(CompletableFuture[]::new); @@ -88,6 +103,13 @@ private Promise(CompletableFuture future) { return new Promise<>(combined); } + public static @NonNull Promise allVoid(@NonNull Collection> promises) { + CompletableFuture[] futures = promises.stream() + .map(Promise::toCompletableFuture) + .toArray(CompletableFuture[]::new); + return new Promise<>(CompletableFuture.allOf(futures)); + } + public @NonNull Promise map(@NonNull Function mapper) { return new Promise<>(future.thenApply(mapper)); } @@ -104,6 +126,10 @@ private Promise(CompletableFuture future) { return flatMap(mapper); } + public @NonNull Promise asVoid() { + return map(ignored -> null); + } + public @NonNull Promise exceptionally(@NonNull Function recover) { return new Promise<>(future.exceptionally(recover)); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java index 5c9f0e5..e8f91fc 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java @@ -14,9 +14,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Optional; public final class Localization { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java index 68a0f5b..0afb77f 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java @@ -17,16 +17,6 @@ public final class Placeholders { private Placeholders() { } - @FunctionalInterface - public interface PlaceholderSupplier { - String get(Object player); - } - - @FunctionalInterface - public interface ConfigPlaceholderSupplier { - String get(Object player, T config); - } - public static void register(@NonNull String key, @NonNull PlaceholderSupplier supplier) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(supplier, "supplier"); @@ -89,4 +79,14 @@ private static String replaceConfigPlaceholders(String text, .replaceAll("([A-Z]+)([A-Z][a-z])", "$1-$2") .toLowerCase(); } + + @FunctionalInterface + public interface PlaceholderSupplier { + String get(Object player); + } + + @FunctionalInterface + public interface ConfigPlaceholderSupplier { + String get(Object player, T config); + } } \ No newline at end of file