diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 5d83fef..8348e06 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -3,6 +3,7 @@ import java.text.SimpleDateFormat import java.util.* val paperApiVersion = "26.1.2" +val supportedMinecraftVersions = listOf("26.1", "26.1.1", "26.1.2", "26.2") plugins { java @@ -132,7 +133,7 @@ tasks { property("buildNumber", if (System.getenv("BUILD_NUMBER") != null) System.getenv("BUILD_NUMBER") else getBuildNumber()) property("date", SimpleDateFormat("MM/dd/yyyy 'at' hh:mm:ss a z").format(Date())) property("gitCommit", indraGit.commit().get().name) - property("minecraftVersion", paperApiVersion) + property("minecraftVersion", supportedMinecraftVersions.joinToString(",")) } } } diff --git a/server/src/main/java/dev/plex/command/impl/DebugCMD.java b/server/src/main/java/dev/plex/command/impl/DebugCMD.java index ebd208e..2a053e1 100644 --- a/server/src/main/java/dev/plex/command/impl/DebugCMD.java +++ b/server/src/main/java/dev/plex/command/impl/DebugCMD.java @@ -5,7 +5,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommandContext; import dev.plex.command.exception.CommandFailException; -import dev.plex.menu.impl.MaterialMenu; +import dev.plex.menu.dialog.MaterialDialog; import dev.plex.util.GameRuleUtil; import dev.plex.util.PlexLog; @@ -126,7 +126,7 @@ public class DebugCMD extends ServerCommand { return context.messageComponent("noPermissionConsole"); } - new MaterialMenu().open(playerSender); + new MaterialDialog().open(playerSender); return null; } return context.usage(); diff --git a/server/src/main/java/dev/plex/command/impl/PlexCMD.java b/server/src/main/java/dev/plex/command/impl/PlexCMD.java index e622e81..add5d4c 100644 --- a/server/src/main/java/dev/plex/command/impl/PlexCMD.java +++ b/server/src/main/java/dev/plex/command/impl/PlexCMD.java @@ -10,6 +10,7 @@ import dev.plex.util.BuildInfo; import dev.plex.util.PlexLog; import dev.plex.util.PlexUtils; import dev.plex.util.TimeUtils; +import dev.plex.util.UpdateChecker; import java.util.List; import java.util.stream.Collectors; @@ -18,6 +19,7 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import net.kyori.adventure.text.Component; import org.apache.commons.lang3.StringUtils; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; public class PlexCMD extends ServerCommand @@ -69,7 +71,7 @@ public class PlexCMD extends ServerCommand context.send(sender, context.mmString("Authors: Telesphoreo, Taahh")); context.send(sender, context.mmString("Built by: " + BuildInfo.getAuthor() + " on " + BuildInfo.getDate())); context.send(sender, context.mmString("Run /plex modules to see a list of modules.")); - plugin.getUpdateChecker().getUpdateStatusMessage(sender, true, 2); + plugin.getApi().scheduler().runAsync(() -> plugin.getUpdateChecker().getUpdateStatusMessage(sender, true, 2)); return null; } if (args[0].equalsIgnoreCase("reload")) @@ -111,12 +113,19 @@ public class PlexCMD extends ServerCommand else if (args[1].equalsIgnoreCase("update")) { context.checkPermission(sender, "plex.modules.update"); - for (PlexModule module : plugin.getModuleManager().getModules()) + plugin.getApi().scheduler().runAsync(() -> { - plugin.getUpdateChecker().updateModuleJar(sender, module); - } - plugin.getModuleManager().reloadModules(); - return context.mmString("All modules updated and reloaded!"); + for (PlexModule module : plugin.getModuleManager().getModules()) + { + plugin.getUpdateChecker().updateModuleJar(sender, module); + } + plugin.getApi().scheduler().runGlobal(() -> + { + plugin.getModuleManager().reloadModules(); + sender.sendMessage(context.mmString("All modules updated and reloaded!")); + }); + }); + return null; } else if (args[1].equalsIgnoreCase("install")) { @@ -126,7 +135,7 @@ public class PlexCMD extends ServerCommand return context.usage(); } String moduleName = args[2]; - plugin.getUpdateChecker().installModuleJar(sender, moduleName); + plugin.getApi().scheduler().runAsync(() -> plugin.getUpdateChecker().installModuleJar(sender, moduleName)); return context.mmString("Installing module " + moduleName + "..."); } else if (args[1].equalsIgnoreCase("uninstall")) @@ -154,12 +163,22 @@ public class PlexCMD extends ServerCommand else if (args[0].equalsIgnoreCase("update")) { context.checkPermission(sender, "plex.update"); - if (!plugin.getUpdateChecker().getUpdateStatusMessage(sender, false, 0)) + plugin.getApi().scheduler().runAsync(() -> { - return context.mmString("Plex is already up to date!"); - } - plugin.getUpdateChecker().updateJar(sender, "Plex", false); - return context.mmString("Alert: Restart the server for the new JAR file to be applied."); + UpdateChecker.UpdateCheckResult result = plugin.getUpdateChecker().checkForUpdates(false); + if (result.status() == UpdateChecker.UpdateCheckStatus.UPDATE_AVAILABLE) + { + plugin.getUpdateChecker().updateJar(sender, result.metadata(), () -> sendMessage(sender, context.mmString("Alert: Restart the server for the new JAR file to be applied."))); + return; + } + if (result.status() == UpdateChecker.UpdateCheckStatus.UP_TO_DATE) + { + sendMessage(sender, context.mmString("Plex is already up to date!")); + return; + } + plugin.getUpdateChecker().sendResultMessage(sender, result, 2); + }); + return null; } else { @@ -167,4 +186,14 @@ public class PlexCMD extends ServerCommand } return null; } + + private void sendMessage(CommandSender sender, Component message) + { + if (sender instanceof Player player) + { + plugin.getApi().scheduler().runEntity(player, () -> sender.sendMessage(message)); + return; + } + plugin.getApi().scheduler().runGlobal(() -> sender.sendMessage(message)); + } } diff --git a/server/src/main/java/dev/plex/command/impl/PunishmentsCMD.java b/server/src/main/java/dev/plex/command/impl/PunishmentsCMD.java index 1ffe21c..b534660 100644 --- a/server/src/main/java/dev/plex/command/impl/PunishmentsCMD.java +++ b/server/src/main/java/dev/plex/command/impl/PunishmentsCMD.java @@ -6,8 +6,7 @@ import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommandContext; import dev.plex.command.exception.PlayerNotFoundException; import dev.plex.command.source.RequiredCommandSource; -import dev.plex.menu.impl.PunishedPlayerMenu; -import dev.plex.menu.impl.PunishmentMenu; +import dev.plex.menu.dialog.PunishmentDialog; import dev.plex.player.PlexPlayer; @@ -45,9 +44,10 @@ public class PunishmentsCMD extends ServerCommand CommandSender sender = context.sender(); Player playerSender = context.player(); String[] args = context.args(); + PunishmentDialog dialog = new PunishmentDialog(plugin.getPlayerService()); if (args.length == 0) { - new PunishmentMenu(plugin.getPlayerService()).open(playerSender); + dialog.open(playerSender); } else { @@ -59,7 +59,7 @@ public class PunishmentsCMD extends ServerCommand final OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(args[0]); final PlexPlayer player = offlinePlayer.isOnline() ? context.getOnlinePlexPlayer(args[0]) : context.getOfflinePlexPlayer(offlinePlayer.getUniqueId()); - new PunishedPlayerMenu(player, plugin.getPlayerService()).open(playerSender); + dialog.open(playerSender, player); } return null; diff --git a/server/src/main/java/dev/plex/command/impl/ToggleCMD.java b/server/src/main/java/dev/plex/command/impl/ToggleCMD.java index 66517d1..b226ca4 100644 --- a/server/src/main/java/dev/plex/command/impl/ToggleCMD.java +++ b/server/src/main/java/dev/plex/command/impl/ToggleCMD.java @@ -3,7 +3,7 @@ package dev.plex.command.impl; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommandContext; -import dev.plex.menu.impl.ToggleMenu; +import dev.plex.menu.dialog.ToggleDialog; import dev.plex.util.PlexUtils; @@ -47,19 +47,8 @@ public class ToggleCMD extends ServerCommand CommandSender sender = context.sender(); Player playerSender = context.player(); String[] args = context.args(); - if (context.isConsole(sender) || playerSender == null) + if (args.length > 0) { - if (args.length == 0) - { - sender.sendMessage(context.messageComponent("toggleAvailable")); - sender.sendMessage(toggleListItem(context, "toggleExplosions", "explosions")); - sender.sendMessage(toggleListItem(context, "toggleFluidSpread", "fluidspread")); - sender.sendMessage(toggleListItem(context, "toggleDrops", "drops")); - sender.sendMessage(toggleListItem(context, "toggleRedstone", "redstone")); - sender.sendMessage(toggleListItem(context, "togglePvp", "pvp")); - sender.sendMessage(toggleListItem(context, "toggleChat", "chat")); - return null; - } switch (args[0].toLowerCase()) { case "explosions" -> @@ -93,7 +82,18 @@ public class ToggleCMD extends ServerCommand } } } - new ToggleMenu(plugin).open(playerSender); + if (context.isConsole(sender) || playerSender == null) + { + sender.sendMessage(context.messageComponent("toggleAvailable")); + sender.sendMessage(toggleListItem(context, "toggleExplosions", "explosions")); + sender.sendMessage(toggleListItem(context, "toggleFluidSpread", "fluidspread")); + sender.sendMessage(toggleListItem(context, "toggleDrops", "drops")); + sender.sendMessage(toggleListItem(context, "toggleRedstone", "redstone")); + sender.sendMessage(toggleListItem(context, "togglePvp", "pvp")); + sender.sendMessage(toggleListItem(context, "toggleChat", "chat")); + return null; + } + new ToggleDialog(plugin).open(playerSender); return null; } diff --git a/server/src/main/java/dev/plex/handlers/ListenerHandler.java b/server/src/main/java/dev/plex/handlers/ListenerHandler.java index 56c27af..0fe1582 100644 --- a/server/src/main/java/dev/plex/handlers/ListenerHandler.java +++ b/server/src/main/java/dev/plex/handlers/ListenerHandler.java @@ -12,7 +12,6 @@ import dev.plex.listener.impl.CommandListener; import dev.plex.listener.impl.DropListener; import dev.plex.listener.impl.FreezeListener; import dev.plex.listener.impl.GameModeListener; -import dev.plex.listener.impl.MenuListener; import dev.plex.listener.impl.MobListener; import dev.plex.listener.impl.MuteListener; import dev.plex.listener.impl.PlayerListener; @@ -50,7 +49,6 @@ public class ListenerHandler register(() -> new DropListener(plugin)); register(() -> new FreezeListener(plugin)); register(() -> new GameModeListener(plugin)); - register(() -> new MenuListener(plugin)); register(() -> new MobListener(plugin)); register(() -> new MuteListener(plugin)); register(() -> new PlayerListener(plugin)); diff --git a/server/src/main/java/dev/plex/listener/impl/MenuListener.java b/server/src/main/java/dev/plex/listener/impl/MenuListener.java deleted file mode 100644 index 6b9181d..0000000 --- a/server/src/main/java/dev/plex/listener/impl/MenuListener.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.plex.listener.impl; - -import dev.plex.Plex; -import dev.plex.listener.ServerListenerBase; -import dev.plex.menu.AbstractMenu; -import dev.plex.menu.pagination.PageableMenu; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.inventory.meta.ItemMeta; - -/** - * @author Taah - * @since 7:01 AM [02-09-2023] - */ -public class MenuListener extends ServerListenerBase -{ - public MenuListener(Plex plugin) - { - super(plugin); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onClick(InventoryClickEvent event) - { - if (event.getClickedInventory() == null) - { - return; - } - - if (event.getCurrentItem() == null) - { - return; - } - - if (!(event.getWhoClicked() instanceof Player player)) - { - return; - } - - AbstractMenu.INVENTORIES.entrySet().stream().filter(entry -> entry.getKey().equals(event.getView().title())) - .findFirst() - .ifPresent(entry -> - { - if (entry.getValue() instanceof PageableMenu.Page page) - { - final ItemMeta meta = event.getCurrentItem().getItemMeta(); - if (meta != null && meta.displayName() != null) - { - if (meta.displayName().equals(PageableMenu.previousItem().getItemMeta().displayName())) - { - page.parent().currentPage(page.parent().currentPage() - 1); - page.parent().open(player, page.parent().currentPage()); - event.setCancelled(true); - return; - } - else if (meta.displayName().equals(PageableMenu.nextItem().getItemMeta().displayName())) - { - page.parent().currentPage(page.parent().currentPage() + 1); - page.parent().open(player, page.parent().currentPage()); - event.setCancelled(true); - return; - } - else if (meta.displayName().equals(PageableMenu.closeItem().getItemMeta().displayName())) - { - player.closeInventory(); - event.setCancelled(true); - return; - } - } - } - event.setCancelled(entry.getValue().onClick(event.getView(), event.getClickedInventory(), player, event.getCurrentItem())); - }); - } -} diff --git a/server/src/main/java/dev/plex/menu/AbstractMenu.java b/server/src/main/java/dev/plex/menu/AbstractMenu.java deleted file mode 100644 index c8293b9..0000000 --- a/server/src/main/java/dev/plex/menu/AbstractMenu.java +++ /dev/null @@ -1,62 +0,0 @@ -package dev.plex.menu; - -import com.google.common.collect.Maps; -import dev.plex.util.minimessage.SafeMiniMessage; - -import java.util.Map; - -import lombok.Getter; -import lombok.experimental.Accessors; -import net.kyori.adventure.text.Component; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; - -@Getter -@Accessors(fluent = true) -public abstract class AbstractMenu -{ - public static final Map INVENTORIES = Maps.newHashMap(); - private final Component name; - private final Inventory inventory; - - public AbstractMenu(Component name, Rows rows) - { - this.name = name; - this.inventory = Bukkit.createInventory(null, rows.slots, name); - - INVENTORIES.put(name, this); - } - - public AbstractMenu(String name, Rows rows) - { - this(SafeMiniMessage.mmDeserializeWithoutEvents(name), rows); - } - - public abstract boolean onClick(InventoryView view, Inventory inventory, Player player, ItemStack clicked); - - public void open(Player player) - { - player.openInventory(this.inventory); - } - - public enum Rows - { - ONE(9), TWO(18), THREE(27), FOUR(36), FIVE(45), SIX(54); - - private final int slots; - - Rows(int slots) - { - this.slots = slots; - } - - public int slots() - { - return this.slots; - } - } - -} diff --git a/server/src/main/java/dev/plex/menu/dialog/MaterialDialog.java b/server/src/main/java/dev/plex/menu/dialog/MaterialDialog.java new file mode 100644 index 0000000..062649b --- /dev/null +++ b/server/src/main/java/dev/plex/menu/dialog/MaterialDialog.java @@ -0,0 +1,103 @@ +package dev.plex.menu.dialog; + +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickCallback; +import org.bukkit.Material; +import org.bukkit.entity.Player; + +public class MaterialDialog +{ + private static final int PAGE_SIZE = 40; + private static final ClickCallback.Options CALLBACK_OPTIONS = ClickCallback.Options.builder() + .uses(1) + .lifetime(Duration.ofMinutes(5)) + .build(); + + private final List materials = Arrays.stream(Material.values()) + .filter(material -> !material.isAir()) + .toList(); + + public void open(Player player) + { + open(player, 0); + } + + private void open(Player player, int page) + { + player.showDialog(create(page)); + } + + private Dialog create(int page) + { + int pageCount = Math.max(1, (int) Math.ceil(materials.size() / (double) PAGE_SIZE)); + int currentPage = Math.max(0, Math.min(page, pageCount - 1)); + int fromIndex = currentPage * PAGE_SIZE; + int toIndex = Math.min(fromIndex + PAGE_SIZE, materials.size()); + List actions = new ArrayList<>(materials.subList(fromIndex, toIndex).stream() + .map(material -> materialButton(material, currentPage)) + .toList()); + if (currentPage > 0) + { + actions.add(pageButton("Previous Page", currentPage - 1)); + } + if (currentPage + 1 < pageCount) + { + actions.add(pageButton("Next Page", currentPage + 1)); + } + + return Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(Component.text("Materials")) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.WAIT_FOR_RESPONSE) + .body(List.of(DialogBody.plainMessage(Component.text("Page " + (currentPage + 1) + " of " + pageCount)))) + .build()) + .type(DialogType.multiAction(actions, closeButton(), 4))); + } + + private ActionButton materialButton(Material material, int page) + { + return ActionButton.builder(Component.text(material.name())) + .width(120) + .action(DialogAction.customClick((response, audience) -> + { + if (audience instanceof Player player) + { + player.sendMessage(Component.text(material.name())); + open(player, page); + } + }, CALLBACK_OPTIONS)) + .build(); + } + + private ActionButton pageButton(String label, int page) + { + return ActionButton.builder(Component.text(label)) + .width(150) + .action(DialogAction.customClick((response, audience) -> + { + if (audience instanceof Player player) + { + open(player, page); + } + }, CALLBACK_OPTIONS)) + .build(); + } + + private ActionButton closeButton() + { + return ActionButton.builder(Component.text("Close")) + .width(150) + .build(); + } +} diff --git a/server/src/main/java/dev/plex/menu/dialog/PunishmentDialog.java b/server/src/main/java/dev/plex/menu/dialog/PunishmentDialog.java new file mode 100644 index 0000000..3032d87 --- /dev/null +++ b/server/src/main/java/dev/plex/menu/dialog/PunishmentDialog.java @@ -0,0 +1,142 @@ +package dev.plex.menu.dialog; + +import dev.plex.player.PlayerService; +import dev.plex.player.PlexPlayer; +import dev.plex.punishment.Punishment; +import dev.plex.util.PlexUtils; +import dev.plex.util.TimeUtils; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickCallback; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +public class PunishmentDialog +{ + private static final ClickCallback.Options CALLBACK_OPTIONS = ClickCallback.Options.builder() + .uses(1) + .lifetime(Duration.ofMinutes(5)) + .build(); + + private final PlayerService playerService; + + public PunishmentDialog(PlayerService playerService) + { + this.playerService = playerService; + } + + public void open(Player player) + { + player.showDialog(playerListDialog()); + } + + public void open(Player viewer, PlexPlayer punishedPlayer) + { + viewer.showDialog(playerPunishmentsDialog(punishedPlayer)); + } + + private Dialog playerListDialog() + { + List actions = Bukkit.getOnlinePlayers().stream() + .map(this::playerButton) + .toList(); + + return Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(PlexUtils.messageComponent("punishmentMenuTitle")) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.WAIT_FOR_RESPONSE) + .body(List.of(DialogBody.plainMessage(Component.text("Select an online player to view their punishments.")))) + .build()) + .type(DialogType.multiAction(actions, closeButton(), 2))); + } + + private Dialog playerPunishmentsDialog(PlexPlayer punishedPlayer) + { + List body = new ArrayList<>(); + List punishments = punishedPlayer.getPunishments(); + + if (punishments.isEmpty()) + { + body.add(DialogBody.plainMessage(Component.text("No punishments found."))); + } + else + { + punishments.forEach(punishment -> body.add(DialogBody.plainMessage(punishmentSummary(punishment), 320))); + } + + return Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(PlexUtils.messageComponent("punishedPlayerMenuTitle", punishedPlayer.getName())) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.WAIT_FOR_RESPONSE) + .body(body) + .build()) + .type(DialogType.multiAction(List.of(backButton()), closeButton(), 2))); + } + + private ActionButton playerButton(Player player) + { + return ActionButton.builder(Component.text(player.getName())) + .width(150) + .action(DialogAction.customClick((response, audience) -> openPunishments(audience, player), CALLBACK_OPTIONS)) + .build(); + } + + private ActionButton backButton() + { + return ActionButton.builder(Component.text("Back")) + .width(150) + .action(DialogAction.customClick((response, audience) -> + { + if (audience instanceof Player player) + { + open(player); + } + }, CALLBACK_OPTIONS)) + .build(); + } + + private ActionButton closeButton() + { + return ActionButton.builder(Component.text("Close")) + .width(150) + .build(); + } + + private void openPunishments(Audience audience, OfflinePlayer selectedPlayer) + { + if (!(audience instanceof Player viewer)) + { + return; + } + + PlexPlayer punishedPlayer = playerService.getPlayer(selectedPlayer.getUniqueId()); + if (punishedPlayer == null) + { + viewer.sendMessage(PlexUtils.messageComponent("punishmentPlayerNotFound")); + return; + } + open(viewer, punishedPlayer); + } + + private Component punishmentSummary(Punishment punishment) + { + String punisher = punishment.getPunisher() == null ? "CONSOLE" : playerService.getNameByUUID(punishment.getPunisher()); + return Component.text(punishment.getType().name() + "\n" + + "By: " + punisher + "\n" + + "Issued: " + TimeUtils.useTimezone(punishment.getIssueDate()) + "\n" + + "Expire(d/s): " + TimeUtils.useTimezone(punishment.getEndDate()) + "\n" + + "Reason: " + punishment.getReason()); + } +} diff --git a/server/src/main/java/dev/plex/menu/dialog/ToggleDialog.java b/server/src/main/java/dev/plex/menu/dialog/ToggleDialog.java new file mode 100644 index 0000000..c55455d --- /dev/null +++ b/server/src/main/java/dev/plex/menu/dialog/ToggleDialog.java @@ -0,0 +1,107 @@ +package dev.plex.menu.dialog; + +import dev.plex.Plex; +import dev.plex.util.PlexUtils; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import java.time.Duration; +import java.util.List; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickCallback; +import org.bukkit.entity.Player; + +public class ToggleDialog +{ + private static final ClickCallback.Options CALLBACK_OPTIONS = ClickCallback.Options.builder() + .uses(1) + .lifetime(Duration.ofMinutes(5)) + .build(); + + private final Plex plugin; + + public ToggleDialog(Plex plugin) + { + this.plugin = plugin; + } + + public void open(Player player) + { + player.showDialog(create()); + } + + private Dialog create() + { + return Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(PlexUtils.messageComponent("toggleMenuTitle")) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.WAIT_FOR_RESPONSE) + .body(List.of( + DialogBody.plainMessage(statusLine("toggleExplosions", "explosions", true)), + DialogBody.plainMessage(statusLine("toggleFluidSpread", "fluidspread", false)), + DialogBody.plainMessage(statusLine("toggleDrops", "drops", false)), + DialogBody.plainMessage(statusLine("toggleRedstone", "redstone", false)), + DialogBody.plainMessage(statusLine("togglePvp", "pvp", false)), + DialogBody.plainMessage(Component.text(PlexUtils.messageString("toggleChat") + ": " + PlexUtils.messageString(plugin.toggles.getBoolean("chat") ? "stateOn" : "stateOff"))))) + .build()) + .type(DialogType.multiAction(List.of( + toggleButton("toggleExplosions", "toggleExplosionsLower", "explosions"), + toggleButton("toggleFluidSpread", "toggleFluidSpreadLower", "fluidspread"), + toggleButton("toggleDrops", "toggleDropsLower", "drops"), + toggleButton("toggleRedstone", "toggleRedstoneLower", "redstone"), + toggleButton("togglePvp", "togglePvpLower", "pvp"), + toggleButton("toggleChat", "toggleChatLower", "chat")), + closeButton(), 2))); + } + + private ActionButton toggleButton(String nameKey, String lowerNameKey, String toggle) + { + return ActionButton.builder(Component.text(PlexUtils.messageString(nameKey))) + .tooltip(Component.text(PlexUtils.messageString(plugin.toggles.getBoolean(toggle) ? "stateEnabled" : "stateDisabled"))) + .width(150) + .action(DialogAction.customClick((response, audience) -> toggle(audience, lowerNameKey, toggle), CALLBACK_OPTIONS)) + .build(); + } + + private ActionButton closeButton() + { + return ActionButton.builder(Component.text("Close")) + .width(150) + .build(); + } + + private void toggle(Audience audience, String lowerNameKey, String toggle) + { + if (!(audience instanceof Player player) || !player.hasPermission("plex.toggle")) + { + return; + } + + plugin.toggles.set(toggle, !plugin.toggles.getBoolean(toggle)); + if ("chat".equals(toggle)) + { + PlexUtils.broadcast(PlexUtils.messageComponent("chatToggled", player.getName(), PlexUtils.messageString(plugin.toggles.getBoolean("chat") ? "stateOn" : "stateOff"))); + } + player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString(lowerNameKey))); + open(player); + } + + private Component statusLine(String nameKey, String toggle, boolean enabledIsUnsafe) + { + return Component.text(PlexUtils.messageString(nameKey) + ": " + status(toggle, enabledIsUnsafe)); + } + + private String status(String toggle, boolean enabledIsUnsafe) + { + if (enabledIsUnsafe) + { + return PlexUtils.messageString(plugin.toggles.getBoolean(toggle) ? "stateEnabledUnsafe" : "stateDisabledSafe"); + } + return PlexUtils.messageString(plugin.toggles.getBoolean(toggle) ? "stateEnabled" : "stateDisabled"); + } +} diff --git a/server/src/main/java/dev/plex/menu/impl/MaterialMenu.java b/server/src/main/java/dev/plex/menu/impl/MaterialMenu.java deleted file mode 100644 index 392d2e3..0000000 --- a/server/src/main/java/dev/plex/menu/impl/MaterialMenu.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.plex.menu.impl; - -import dev.plex.menu.AbstractMenu; -import dev.plex.menu.pagination.PageableMenu; -import dev.plex.util.item.ItemBuilder; - -import java.util.Arrays; -import java.util.List; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -public class MaterialMenu extends PageableMenu -{ - public MaterialMenu() - { - super("Materials", AbstractMenu.Rows.SIX); - onClick((inventoryView, inventory, player, itemStack) -> - { - player.sendMessage(itemStack.displayName()); - return true; - }); - this.init(); - } - - @Override - protected ItemStack toItem(Material object) - { - return new ItemBuilder(object).displayName("" + object.name()).build(); - } - - @Override - protected List list() - { - return Arrays.stream(Material.values()).filter(material -> !material.isAir()).toList(); - } -} diff --git a/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java b/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java deleted file mode 100644 index bf2ceb0..0000000 --- a/server/src/main/java/dev/plex/menu/impl/PunishedPlayerMenu.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.plex.menu.impl; - -import dev.plex.menu.AbstractMenu; -import dev.plex.menu.pagination.PageableMenu; -import dev.plex.player.PlayerService; -import dev.plex.player.PlexPlayer; -import dev.plex.punishment.Punishment; -import dev.plex.util.PlexUtils; -import dev.plex.util.TimeUtils; -import dev.plex.util.item.ItemBuilder; - -import java.util.List; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -public class PunishedPlayerMenu extends PageableMenu -{ - private final PlexPlayer punishedPlayer; - private final PlayerService playerService; - - public PunishedPlayerMenu(PlexPlayer player, PlayerService playerService) - { - super(PlexUtils.messageComponent("punishedPlayerMenuTitle", player.getName()), AbstractMenu.Rows.SIX); - this.punishedPlayer = player; - this.playerService = playerService; - onClick((inventoryView, itemStacks, player1, itemStack) -> true); - this.init(); - } - - @Override - protected ItemStack toItem(Punishment object) - { - return new ItemBuilder(Material.PAPER) - .displayName(PlexUtils.messageComponent("punishmentItemTitle", object.getType().name())) - .lore(PlexUtils.messageComponent("punishmentItemPunisher", object.getPunisher() == null ? "CONSOLE" : playerService.getNameByUUID(object.getPunisher())), - PlexUtils.messageComponent("punishmentItemIssued", TimeUtils.useTimezone(object.getIssueDate())), - PlexUtils.messageComponent("punishmentItemExpires", TimeUtils.useTimezone(object.getEndDate())), - PlexUtils.messageComponent("punishmentItemReason", object.getReason())) - .build(); - } - - @Override - protected List list() - { - return this.punishedPlayer.getPunishments(); - } -} diff --git a/server/src/main/java/dev/plex/menu/impl/PunishmentMenu.java b/server/src/main/java/dev/plex/menu/impl/PunishmentMenu.java deleted file mode 100644 index 73f34dc..0000000 --- a/server/src/main/java/dev/plex/menu/impl/PunishmentMenu.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.plex.menu.impl; - -import dev.plex.menu.AbstractMenu; -import dev.plex.menu.pagination.PageableMenu; -import dev.plex.player.PlayerService; -import dev.plex.player.PlexPlayer; -import dev.plex.util.PlexLog; -import dev.plex.util.PlexUtils; -import dev.plex.util.item.ItemBuilder; - -import java.util.List; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; - -public class PunishmentMenu extends PageableMenu -{ - private final PlayerService playerService; - - public PunishmentMenu(PlayerService playerService) - { - super(PlexUtils.messageComponent("punishmentMenuTitle"), AbstractMenu.Rows.SIX); - this.playerService = playerService; - PlexLog.debug("list: {0}", list().size()); - onClick((inventoryView, itemStacks, player, itemStack) -> - { - if (itemStack.getType() != Material.PLAYER_HEAD) - { - return true; - } - final SkullMeta meta = (SkullMeta) itemStack.getItemMeta(); - if (meta.getOwningPlayer() == null) - { - meta.setOwningPlayer(Bukkit.getOfflinePlayer("markbyron")); - } - PlexPlayer punishedPlayer = playerService.getPlayer(meta.getOwningPlayer().getUniqueId()); - if (punishedPlayer == null) - { - player.sendMessage(PlexUtils.messageComponent("punishmentPlayerNotFound")); - player.closeInventory(); - return true; - } - new PunishedPlayerMenu(punishedPlayer, playerService).open(player); - return true; - }); - - this.init(); - } - - @Override - protected ItemStack toItem(Player object) - { - return new ItemBuilder(Material.PLAYER_HEAD).owner(object).displayName(PlexUtils.messageComponent("punishmentPlayerItem", object.getName())).build(); - } - - @Override - protected List list() - { - return Bukkit.getOnlinePlayers().stream().map(OfflinePlayer::getPlayer).toList(); - } -} diff --git a/server/src/main/java/dev/plex/menu/impl/ToggleMenu.java b/server/src/main/java/dev/plex/menu/impl/ToggleMenu.java deleted file mode 100644 index 0685b5b..0000000 --- a/server/src/main/java/dev/plex/menu/impl/ToggleMenu.java +++ /dev/null @@ -1,149 +0,0 @@ -package dev.plex.menu.impl; - -import dev.plex.Plex; -import dev.plex.menu.AbstractMenu; -import dev.plex.util.PlexUtils; - -import java.util.List; - -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemFlag; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -public class ToggleMenu extends AbstractMenu -{ - private final Plex plugin; - - public ToggleMenu(Plex plugin) - { - super(PlexUtils.messageComponent("toggleMenuTitle"), Rows.ONE); - this.plugin = plugin; - resetExplosionItem(this.inventory()); - resetFluidspreadItem(this.inventory()); - resetDropsItem(this.inventory()); - resetRedstoneItem(this.inventory()); - resetPVPItem(this.inventory()); - resetChatItem(this.inventory()); - } - - private void resetExplosionItem(Inventory inventory) - { - ItemStack explosions = new ItemStack(Material.TNT); - ItemMeta explosionsItemMeta = explosions.getItemMeta(); - explosionsItemMeta.displayName(PlexUtils.messageComponent("toggleMenuExplosionsName")); - explosionsItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuExplosionsLore", status("explosions", true)))); - explosions.setItemMeta(explosionsItemMeta); - inventory.setItem(0, explosions); - } - - private void resetFluidspreadItem(Inventory inventory) - { - ItemStack water = new ItemStack(Material.WATER_BUCKET); - ItemMeta waterItemMeta = water.getItemMeta(); - waterItemMeta.displayName(PlexUtils.messageComponent("toggleMenuFluidSpreadName")); - waterItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuFluidSpreadLore", status("fluidspread", false)))); - water.setItemMeta(waterItemMeta); - inventory.setItem(1, water); - } - - private void resetDropsItem(Inventory inventory) - { - ItemStack feather = new ItemStack(Material.FEATHER); - ItemMeta featherItemMeta = feather.getItemMeta(); - featherItemMeta.displayName(PlexUtils.messageComponent("toggleMenuDropsName")); - featherItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuDropsLore", status("drops", false)))); - feather.setItemMeta(featherItemMeta); - inventory.setItem(2, feather); - } - - private void resetRedstoneItem(Inventory inventory) - { - ItemStack redstone = new ItemStack(Material.REDSTONE); - ItemMeta redstoneItemMeta = redstone.getItemMeta(); - redstoneItemMeta.displayName(PlexUtils.messageComponent("toggleMenuRedstoneName")); - redstoneItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuRedstoneLore", status("redstone", false)))); - redstone.setItemMeta(redstoneItemMeta); - inventory.setItem(3, redstone); - } - - private void resetPVPItem(Inventory inventory) - { - ItemStack pvp = new ItemStack(Material.IRON_SWORD); - ItemMeta pvpItemMeta = pvp.getItemMeta(); - pvpItemMeta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); - pvpItemMeta.displayName(PlexUtils.messageComponent("toggleMenuPvpName")); - pvpItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuPvpLore", status("pvp", false)))); - pvp.setItemMeta(pvpItemMeta); - inventory.setItem(4, pvp); - } - - private void resetChatItem(Inventory inventory) - { - ItemStack chat = new ItemStack(Material.OAK_SIGN); - ItemMeta chatItemMeta = chat.getItemMeta(); - chatItemMeta.displayName(PlexUtils.messageComponent("toggleMenuChatName")); - chatItemMeta.lore(List.of(PlexUtils.messageComponent("toggleMenuChatLore", PlexUtils.messageString(plugin.toggles.getBoolean("chat") ? "stateOn" : "stateOff")))); - chat.setItemMeta(chatItemMeta); - inventory.setItem(5, chat); - } - - @Override - public boolean onClick(InventoryView view, Inventory inventory, Player player, ItemStack clicked) - { - if (!player.hasPermission("plex.toggle")) - { - return false; - } - if (clicked.getType() == Material.TNT) - { - plugin.toggles.set("explosions", !plugin.toggles.getBoolean("explosions")); - resetExplosionItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("toggleExplosionsLower"))); - } - if (clicked.getType() == Material.WATER_BUCKET) - { - plugin.toggles.set("fluidspread", !plugin.toggles.getBoolean("fluidspread")); - resetFluidspreadItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("toggleFluidSpreadLower"))); - } - if (clicked.getType() == Material.FEATHER) - { - plugin.toggles.set("drops", !plugin.toggles.getBoolean("drops")); - resetDropsItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("toggleDropsLower"))); - } - if (clicked.getType() == Material.REDSTONE) - { - plugin.toggles.set("redstone", !plugin.toggles.getBoolean("redstone")); - resetRedstoneItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("toggleRedstoneLower"))); - } - if (clicked.getType() == Material.IRON_SWORD) - { - plugin.toggles.set("pvp", !plugin.toggles.getBoolean("pvp")); - resetPVPItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("togglePvpLower"))); - } - if (clicked.getType() == Material.OAK_SIGN) - { - plugin.toggles.set("chat", !plugin.toggles.getBoolean("chat")); - PlexUtils.broadcast(PlexUtils.messageComponent("chatToggled", player.getName(), PlexUtils.messageString(plugin.toggles.getBoolean("chat") ? "stateOn" : "stateOff"))); - resetChatItem(inventory); - player.sendMessage(PlexUtils.messageComponent("toggleToggled", PlexUtils.messageString("toggleChatLower"))); - } - return true; - } - - private String status(String toggle, boolean enabledIsUnsafe) - { - if (enabledIsUnsafe) - { - return PlexUtils.messageString(plugin.toggles.getBoolean(toggle) ? "stateEnabledUnsafe" : "stateDisabledSafe"); - } - return PlexUtils.messageString(plugin.toggles.getBoolean(toggle) ? "stateEnabled" : "stateDisabled"); - } -} diff --git a/server/src/main/java/dev/plex/menu/pagination/PageableMenu.java b/server/src/main/java/dev/plex/menu/pagination/PageableMenu.java deleted file mode 100644 index 46feed8..0000000 --- a/server/src/main/java/dev/plex/menu/pagination/PageableMenu.java +++ /dev/null @@ -1,183 +0,0 @@ -package dev.plex.menu.pagination; - -import com.google.common.collect.Maps; -import dev.plex.menu.AbstractMenu; -import dev.plex.util.PlexUtils; -import dev.plex.util.function.ConditionalQuadConsumer; -import dev.plex.util.item.ItemBuilder; -import dev.plex.util.minimessage.SafeMiniMessage; - -import java.util.List; -import java.util.Map; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import net.kyori.adventure.text.Component; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; - -@Getter -@Setter -@Accessors(fluent = true) -public abstract class PageableMenu -{ - protected final Map pages = Maps.newHashMap(); - private final Component name; - private final AbstractMenu.Rows rows; - private final Inventory inventory; - private int currentPage; - private boolean initialized; - - @Setter(AccessLevel.NONE) - private ConditionalQuadConsumer onClick; - - public PageableMenu(Component name, AbstractMenu.Rows rows) - { - this.name = name; - this.rows = rows; - this.inventory = Bukkit.createInventory(null, rows.slots(), name); - - if (rows.slots() < AbstractMenu.Rows.TWO.slots()) - { - throw new RuntimeException("A pageable menu must have at least two rows to compensate for the navigation!"); - } - } - - protected void init() - { - this.initialized = true; - // Preload all needed pages, a better solution is probably just to dynamically create the pages as they go but no! - if (list().size() < (rows.slots() - 9)) - { - final Page page = new Page(name.append(Component.space()).append(PlexUtils.mmDeserialize("(1)")), rows); - page.parent = this; - page.inventory().setItem(rows.slots() - 5, closeItem()); - this.pages.put(0, page); - } - else - { - for (int i = 0; i < Math.ceil(list().size() / (double) (rows.slots() - 9)); i++) - { - final Page page = new Page(name.append(Component.space()).append(PlexUtils.mmDeserialize("(" + (i + 1) + ")")), rows); - page.parent = this; - if (i > 0) // If not first page set previous page button - { - page.inventory().setItem(rows.slots() - 6, previousItem()); - } - page.inventory().setItem(rows.slots() - 5, closeItem()); - if (i < (list().size() / (rows.slots() - 9)) - 1) // If not last page set next page button - { - page.inventory().setItem(rows.slots() - 4, nextItem()); - } - this.pages.put(i, page); - } - } - - int currentSlotIndex = 0; - int currentPageIndex = 0; - int currentIndex = 0; - while (currentSlotIndex < rows.slots() - 9 && currentIndex < list().size()) - { - final Page page = this.pages.get(currentPageIndex); - if (page == null) - { - return; - } - page.inventory().setItem(currentSlotIndex, toItem(list().get(currentIndex))); - currentIndex++; - currentSlotIndex++; - if (currentSlotIndex == rows.slots() - 9) - { - currentSlotIndex = 0; - currentPageIndex++; - } - } - if (onClick != null) // To make sure this wasn't declared already - { - this.pages.forEach((integer, page) -> page.onClick(this.onClick)); - } - } - - public PageableMenu(String name, AbstractMenu.Rows rows) - { - this(SafeMiniMessage.mmDeserializeWithoutEvents(name), rows); - } - - protected abstract ItemStack toItem(T object); - - protected abstract List list(); - - public void open(Player player) - { - open(player, 0); - } - - public void open(Player player, int pageNum) - { - if (!this.initialized) - { - player.sendMessage(PlexUtils.messageComponent("pageableNotInitialized")); - return; - } - final Page page = this.pages.get(pageNum); - if (page == null) - { - player.sendMessage(PlexUtils.messageComponent("pageNotFound")); - return; - } - player.openInventory(page.inventory()); - } - - public void onClick(ConditionalQuadConsumer onClick) - { - this.onClick = onClick; - this.pages.forEach((integer, page) -> page.onClick(this.onClick)); - } - - public static ItemStack nextItem() - { - return new ItemBuilder(Material.FEATHER).displayName(PlexUtils.messageComponent("pageNext")).build(); - } - - public static ItemStack previousItem() - { - return new ItemBuilder(Material.FEATHER).displayName(PlexUtils.messageComponent("pagePrevious")).build(); - } - - public static ItemStack closeItem() - { - return new ItemBuilder(Material.BARRIER).displayName(PlexUtils.messageComponent("pageClose")).build(); - } - - - @Getter - @Setter - @Accessors(fluent = true) - public static class Page extends AbstractMenu - { - private ConditionalQuadConsumer onClick; - private PageableMenu parent; - - private Page(Component name, Rows rows) - { - super(name, rows); - } - - private Page(String name, AbstractMenu.Rows rows) - { - this(SafeMiniMessage.mmDeserializeWithoutEvents(name), rows); - } - - @Override - public boolean onClick(InventoryView view, Inventory inventory, Player player, ItemStack clicked) - { - return onClick.accept(view, inventory, player, clicked); - } - } -} diff --git a/server/src/main/java/dev/plex/services/impl/UpdateCheckerService.java b/server/src/main/java/dev/plex/services/impl/UpdateCheckerService.java index 3bfd474..4e67841 100644 --- a/server/src/main/java/dev/plex/services/impl/UpdateCheckerService.java +++ b/server/src/main/java/dev/plex/services/impl/UpdateCheckerService.java @@ -2,12 +2,13 @@ package dev.plex.services.impl; import dev.plex.Plex; import dev.plex.services.AbstractService; +import dev.plex.util.UpdateChecker; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import org.bukkit.Bukkit; public class UpdateCheckerService extends AbstractService { - private boolean newVersion = false; + private boolean notified = false; public UpdateCheckerService(Plex plugin) { @@ -17,11 +18,16 @@ public class UpdateCheckerService extends AbstractService @Override public void run(ScheduledTask task) { - if (!newVersion) + if (!notified) { - if (plugin.getUpdateChecker().getUpdateStatusMessage(Bukkit.getConsoleSender(), false, 1)) + UpdateChecker.UpdateCheckResult result = plugin.getUpdateChecker().checkForUpdates(false); + plugin.getUpdateChecker().sendResultMessage(Bukkit.getConsoleSender(), result, 1); + if (result.status() == UpdateChecker.UpdateCheckStatus.UPDATE_AVAILABLE + || result.status() == UpdateChecker.UpdateCheckStatus.MINECRAFT_TOO_OLD + || result.status() == UpdateChecker.UpdateCheckStatus.MINECRAFT_TOO_NEW + || result.status() == UpdateChecker.UpdateCheckStatus.MINECRAFT_UNLISTED) { - newVersion = true; + notified = true; } } } diff --git a/server/src/main/java/dev/plex/updater/ArtifactMetadata.java b/server/src/main/java/dev/plex/updater/ArtifactMetadata.java index da07b0a..706bc58 100644 --- a/server/src/main/java/dev/plex/updater/ArtifactMetadata.java +++ b/server/src/main/java/dev/plex/updater/ArtifactMetadata.java @@ -73,7 +73,12 @@ public final class ArtifactMetadata return publishedAt; } - public Optional validatePlex(UpdateChannel requestedChannel, String minecraftVersion) + public List minecraftVersions() + { + return minecraftVersions == null ? List.of() : List.copyOf(minecraftVersions); + } + + public Optional validatePlex(UpdateChannel requestedChannel) { Optional commonError = validateCommon(requestedChannel); if (commonError.isPresent()) @@ -84,9 +89,9 @@ public final class ArtifactMetadata { return Optional.of("Plex metadata has unexpected artifact name " + name); } - if (minecraftVersions == null || !minecraftVersions.contains(minecraftVersion)) + if (minecraftVersions == null || minecraftVersions.isEmpty()) { - return Optional.of("metadata does not include Minecraft version " + minecraftVersion); + return Optional.of("Plex metadata is missing minecraftVersions"); } if (apiCompatibility == null) { @@ -95,6 +100,20 @@ public final class ArtifactMetadata return Optional.empty(); } + public Optional validatePlex(UpdateChannel requestedChannel, String minecraftVersion) + { + Optional plexError = validatePlex(requestedChannel); + if (plexError.isPresent()) + { + return plexError; + } + if (!supportsMinecraftVersion(minecraftVersion)) + { + return Optional.of("metadata does not include Minecraft version " + minecraftVersion); + } + return Optional.empty(); + } + public Optional validateModule(UpdateChannel requestedChannel, String moduleName, int apiCompatibility) { Optional commonError = validateCommon(requestedChannel); @@ -185,6 +204,11 @@ public final class ArtifactMetadata return Optional.empty(); } + private boolean supportsMinecraftVersion(String minecraftVersion) + { + return minecraftVersions != null && minecraftVersions.contains(minecraftVersion); + } + private static boolean isBlank(String value) { return value == null || value.isBlank(); diff --git a/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java b/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java index 63e77d9..939c150 100644 --- a/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java +++ b/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java @@ -37,6 +37,18 @@ public final class UpdateMetadataClient return metadata; } + public ArtifactMetadata fetchPlexChannelLatest() throws MetadataException + { + String path = "/api/v1/projects/Plex/channels/" + channel.id() + "/latest.json"; + ArtifactMetadata metadata = fetch(path); + Optional validationError = metadata.validatePlex(channel); + if (validationError.isPresent()) + { + throw new MetadataException(validationError.get(), false); + } + return metadata; + } + public ArtifactMetadata fetchModuleLatest(String moduleName, int apiCompatibility) throws MetadataException { return fetchModuleLatest(moduleName, apiCompatibility, List.of()); @@ -158,6 +170,11 @@ public final class UpdateMetadataClient { private final boolean notFound; + public static MetadataException localError(String message) + { + return new MetadataException(message, false); + } + private MetadataException(String message, boolean notFound) { super(message); diff --git a/server/src/main/java/dev/plex/util/UpdateChecker.java b/server/src/main/java/dev/plex/util/UpdateChecker.java index 14cabde..62995fb 100644 --- a/server/src/main/java/dev/plex/util/UpdateChecker.java +++ b/server/src/main/java/dev/plex/util/UpdateChecker.java @@ -18,6 +18,7 @@ import java.nio.file.StandardCopyOption; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.HexFormat; import java.util.List; @@ -36,6 +37,18 @@ public class UpdateChecker private ArtifactMetadata latestPlexMetadata; private UpdateMetadataClient.MetadataException latestPlexMetadataFailure; private long latestPlexMetadataFailureAtMillis; + private ArtifactMetadata latestPlexChannelLatestMetadata; + private UpdateMetadataClient.MetadataException latestPlexChannelLatestMetadataFailure; + private long latestPlexChannelLatestMetadataFailureAtMillis; + + public enum UpdateCheckStatus + { + UP_TO_DATE, UPDATE_AVAILABLE, MINECRAFT_TOO_OLD, MINECRAFT_TOO_NEW, MINECRAFT_UNLISTED, ERROR + } + + public record UpdateCheckResult(UpdateCheckStatus status, ArtifactMetadata metadata, UpdateMetadataClient.MetadataException error) + { + } public UpdateChecker(Plex plugin) { @@ -44,56 +57,147 @@ public class UpdateChecker this.metadataClient = new UpdateMetadataClient(channel); } + public UpdateCheckResult checkForUpdates(boolean useCache) + { + String runningMinecraftVersion = getRunningMinecraftVersion(); + if (runningMinecraftVersion == null || runningMinecraftVersion.isBlank() || "unknown".equalsIgnoreCase(runningMinecraftVersion)) + { + return new UpdateCheckResult(UpdateCheckStatus.ERROR, null, UpdateMetadataClient.MetadataException.localError("running Minecraft version could not be determined")); + } + + try + { + ArtifactMetadata metadata = fetchLatestPlexMetadata(useCache, runningMinecraftVersion); + if (metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit())) + { + return new UpdateCheckResult(UpdateCheckStatus.UP_TO_DATE, metadata, null); + } + return new UpdateCheckResult(UpdateCheckStatus.UPDATE_AVAILABLE, metadata, null); + } + catch (UpdateMetadataClient.MetadataException e) + { + if (!e.notFound()) + { + return new UpdateCheckResult(UpdateCheckStatus.ERROR, null, e); + } + } + + ArtifactMetadata channelLatest; + try + { + channelLatest = fetchPlexChannelLatestMetadata(useCache); + } + catch (UpdateMetadataClient.MetadataException e) + { + return new UpdateCheckResult(UpdateCheckStatus.ERROR, null, e); + } + + if (channelLatest.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit())) + { + return new UpdateCheckResult(UpdateCheckStatus.UP_TO_DATE, channelLatest, null); + } + + List supportedVersions = sortedSupportedVersions(channelLatest); + if (!supportedVersions.isEmpty()) + { + String minimumVersion = supportedVersions.get(0); + String maximumVersion = supportedVersions.get(supportedVersions.size() - 1); + if (compareMinecraftVersions(runningMinecraftVersion, minimumVersion) < 0) + { + return new UpdateCheckResult(UpdateCheckStatus.MINECRAFT_TOO_OLD, channelLatest, null); + } + if (compareMinecraftVersions(runningMinecraftVersion, maximumVersion) > 0) + { + return new UpdateCheckResult(UpdateCheckStatus.MINECRAFT_TOO_NEW, channelLatest, null); + } + } + return new UpdateCheckResult(UpdateCheckStatus.MINECRAFT_UNLISTED, channelLatest, null); + } + + public void sendResultMessage(CommandSender sender, UpdateCheckResult result, int verbosity) + { + switch (result.status()) + { + case UP_TO_DATE: + if (verbosity == 2) + { + sendMessage(sender, PlexUtils.messageComponent("updateUpToDate", channel.id())); + } + break; + case UPDATE_AVAILABLE: + if (verbosity >= 1) + { + sendMessage(sender, PlexUtils.messageComponent("updateAvailable", result.metadata().version(), channel.id())); + sendMessage(sender, PlexUtils.messageComponent("updateRunCommand")); + } + break; + case MINECRAFT_TOO_OLD: + if (verbosity >= 1) + { + List supportedVersions = sortedSupportedVersions(result.metadata()); + sendMessage(sender, PlexUtils.messageComponent("updateRequiresNewerMinecraft", + result.metadata().version(), channel.id(), firstSupportedVersion(supportedVersions), getRunningMinecraftVersion(), String.join(", ", supportedVersions))); + } + break; + case MINECRAFT_TOO_NEW: + if (verbosity >= 1) + { + List supportedVersions = sortedSupportedVersions(result.metadata()); + sendMessage(sender, PlexUtils.messageComponent("updateUnsupportedNewerMinecraft", + result.metadata().version(), channel.id(), lastSupportedVersion(supportedVersions), getRunningMinecraftVersion(), String.join(", ", supportedVersions))); + } + break; + case MINECRAFT_UNLISTED: + if (verbosity >= 1) + { + List supportedVersions = sortedSupportedVersions(result.metadata()); + sendMessage(sender, PlexUtils.messageComponent("updateRequiresMinecraftVersion", + result.metadata().version(), channel.id(), String.join(", ", supportedVersions), getRunningMinecraftVersion())); + } + break; + case ERROR: + UpdateMetadataClient.MetadataException error = result.error(); + if (error.notFound()) + { + if (verbosity == 2) + { + sendMessage(sender, PlexUtils.messageComponent("updateMetadataNotFound", channel.id())); + } + break; + } + if (verbosity >= 1) + { + sendMessage(sender, updateMetadataErrorComponent(error)); + PlexLog.error("Unable to check for updates: {0}", error.getMessage()); + if (error.getCause() != null) + { + error.getCause().printStackTrace(); + } + } + break; + } + } + // If verbose is 0, it will display nothing // If verbose is 1, it will only display a message if there is an update available // If verbose is 2, it will display all messages public boolean getUpdateStatusMessage(CommandSender sender, boolean cached, int verbosity) { - try - { - ArtifactMetadata metadata = fetchLatestPlexMetadata(cached); - if (metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit())) - { - if (verbosity == 2) - { - sendMessage(sender, PlexUtils.messageComponent("updateUpToDate", channel.id())); - } - return false; - } - - if (verbosity >= 1) - { - sendMessage(sender, PlexUtils.messageComponent("updateAvailable", metadata.version(), channel.id())); - sendMessage(sender, PlexUtils.messageComponent("updateRunCommand")); - } - return true; - } - catch (UpdateMetadataClient.MetadataException e) - { - if (verbosity == 2 || (verbosity >= 1 && !e.notFound())) - { - sendMessage(sender, updateMetadataErrorComponent(e)); - } - if (!e.notFound()) - { - PlexLog.error("Unable to check for updates: {0}", e.getMessage()); - if (e.getCause() != null) - { - e.getCause().printStackTrace(); - } - } - return false; - } + UpdateCheckResult result = checkForUpdates(cached); + sendResultMessage(sender, result, verbosity); + return result.status() == UpdateCheckStatus.UPDATE_AVAILABLE; } - public void updateJar(CommandSender sender, String name, boolean module) + public void updateJar(CommandSender sender, ArtifactMetadata metadata, Runnable onSuccess) { - updateJar(sender, name, module, List.of()); + File copyTo = new File(Bukkit.getUpdateFolderFile(), metadata.fileName()); + sendMessage(sender, PlexUtils.messageComponent("updateDownloading", metadata.fileName())); + plugin.getApi().scheduler().runAsync(() -> downloadAndInstall(sender, metadata, copyTo, onSuccess)); } public void installModuleJar(CommandSender sender, String name) { - updateJar(sender, name, true, List.of(), () -> plugin.getApi().scheduler().runGlobal(() -> + updateJar(sender, name, List.of(), () -> plugin.getApi().scheduler().runGlobal(() -> { plugin.getModuleManager().reloadModules(); sendMessage(sender, PlexUtils.messageComponent("moduleRestartRequired")); @@ -108,31 +212,20 @@ public class UpdateChecker sendMessage(sender, PlexUtils.messageComponent("moduleUpdateDisabled", moduleFile.getName())); return; } - updateJar(sender, moduleFile.getName(), true, moduleFile.getUpdateUrls()); + updateJar(sender, moduleFile.getName(), moduleFile.getUpdateUrls()); } - private void updateJar(CommandSender sender, String name, boolean module, List moduleUpdateUrls) + private void updateJar(CommandSender sender, String name, List moduleUpdateUrls) { - updateJar(sender, name, module, moduleUpdateUrls, null); + updateJar(sender, name, moduleUpdateUrls, null); } - private void updateJar(CommandSender sender, String name, boolean module, List moduleUpdateUrls, Runnable onSuccess) + private void updateJar(CommandSender sender, String name, List moduleUpdateUrls, Runnable onSuccess) { try { - ArtifactMetadata metadata = module - ? metadataClient.fetchModuleLatest(name, plugin.getApi().compatibility().version(), moduleUpdateUrls) - : fetchLatestPlexMetadata(false); - - if (!module && metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit())) - { - sendMessage(sender, PlexUtils.messageComponent("updateAlreadyUpToDate", channel.id())); - return; - } - - File copyTo = module - ? new File(plugin.getModulesFolder(), metadata.fileName()) - : new File(Bukkit.getUpdateFolderFile(), metadata.fileName()); + ArtifactMetadata metadata = metadataClient.fetchModuleLatest(name, plugin.getApi().compatibility().version(), moduleUpdateUrls); + File copyTo = new File(plugin.getModulesFolder(), metadata.fileName()); sendMessage(sender, PlexUtils.messageComponent("updateDownloading", metadata.fileName())); plugin.getApi().scheduler().runAsync(() -> downloadAndInstall(sender, metadata, copyTo, onSuccess)); @@ -151,15 +244,15 @@ public class UpdateChecker } } - private synchronized ArtifactMetadata fetchLatestPlexMetadata(boolean cached) throws UpdateMetadataClient.MetadataException + private synchronized ArtifactMetadata fetchLatestPlexMetadata(boolean useCache, String runningMinecraftVersion) throws UpdateMetadataClient.MetadataException { - if (cached) + if (useCache) { if (latestPlexMetadata != null) { return latestPlexMetadata; } - if (latestPlexMetadataFailure != null && isPlexMetadataFailureCacheFresh()) + if (latestPlexMetadataFailure != null && isPlexMetadataFailureCacheFresh(latestPlexMetadataFailureAtMillis)) { throw latestPlexMetadataFailure; } @@ -167,7 +260,7 @@ public class UpdateChecker try { - latestPlexMetadata = metadataClient.fetchPlexLatest(BuildInfo.getMinecraftVersion()); + latestPlexMetadata = metadataClient.fetchPlexLatest(runningMinecraftVersion); latestPlexMetadataFailure = null; latestPlexMetadataFailureAtMillis = 0L; return latestPlexMetadata; @@ -183,9 +276,116 @@ public class UpdateChecker } } - private boolean isPlexMetadataFailureCacheFresh() + private synchronized ArtifactMetadata fetchPlexChannelLatestMetadata(boolean useCache) throws UpdateMetadataClient.MetadataException { - return System.currentTimeMillis() - latestPlexMetadataFailureAtMillis < PLEX_METADATA_FAILURE_CACHE_MILLIS; + if (useCache) + { + if (latestPlexChannelLatestMetadata != null) + { + return latestPlexChannelLatestMetadata; + } + if (latestPlexChannelLatestMetadataFailure != null && isPlexMetadataFailureCacheFresh(latestPlexChannelLatestMetadataFailureAtMillis)) + { + throw latestPlexChannelLatestMetadataFailure; + } + } + + try + { + latestPlexChannelLatestMetadata = metadataClient.fetchPlexChannelLatest(); + latestPlexChannelLatestMetadataFailure = null; + latestPlexChannelLatestMetadataFailureAtMillis = 0L; + return latestPlexChannelLatestMetadata; + } + catch (UpdateMetadataClient.MetadataException e) + { + if (latestPlexChannelLatestMetadata == null) + { + latestPlexChannelLatestMetadataFailure = e; + latestPlexChannelLatestMetadataFailureAtMillis = System.currentTimeMillis(); + } + throw e; + } + } + + private List sortedSupportedVersions(ArtifactMetadata metadata) + { + List supportedVersions = new ArrayList<>(metadata.minecraftVersions()); + supportedVersions.sort(UpdateChecker::compareMinecraftVersions); + return supportedVersions; + } + + private String firstSupportedVersion(List supportedVersions) + { + return supportedVersions.isEmpty() ? "unknown" : supportedVersions.get(0); + } + + private String lastSupportedVersion(List supportedVersions) + { + return supportedVersions.isEmpty() ? "unknown" : supportedVersions.get(supportedVersions.size() - 1); + } + + private static int compareMinecraftVersions(String left, String right) + { + String[] leftParts = left.split("\\."); + String[] rightParts = right.split("\\."); + int length = Math.max(leftParts.length, rightParts.length); + for (int i = 0; i < length; i++) + { + int leftPart = i < leftParts.length ? parseVersionPart(leftParts[i]) : 0; + int rightPart = i < rightParts.length ? parseVersionPart(rightParts[i]) : 0; + if (leftPart != rightPart) + { + return Integer.compare(leftPart, rightPart); + } + } + return 0; + } + + private static int parseVersionPart(String part) + { + try + { + return Integer.parseInt(part); + } + catch (NumberFormatException ignored) + { + return -1; + } + } + + private boolean isPlexMetadataFailureCacheFresh(long failureAtMillis) + { + return System.currentTimeMillis() - failureAtMillis < PLEX_METADATA_FAILURE_CACHE_MILLIS; + } + + private String getRunningMinecraftVersion() + { + String version = Bukkit.getBukkitVersion(); + int buildIndex = version.indexOf(".build"); + if (buildIndex >= 0) + { + return version.substring(0, buildIndex); + } + + int separatorIndex = version.indexOf('-'); + if (separatorIndex >= 0) + { + return version.substring(0, separatorIndex); + } + + String buildMinecraftVersion = BuildInfo.getMinecraftVersion(); + int rangeIndex = buildMinecraftVersion.indexOf(" - "); + if (rangeIndex >= 0) + { + return buildMinecraftVersion.substring(0, rangeIndex); + } + int listIndex = buildMinecraftVersion.indexOf(','); + if (listIndex >= 0) + { + return buildMinecraftVersion.substring(0, listIndex); + } + return buildMinecraftVersion; } private void downloadAndInstall(CommandSender sender, ArtifactMetadata metadata, File copyTo, Runnable onSuccess) diff --git a/server/src/main/resources/messages.yml b/server/src/main/resources/messages.yml index d5195ba..a62e0d6 100644 --- a/server/src/main/resources/messages.yml +++ b/server/src/main/resources/messages.yml @@ -220,24 +220,6 @@ togglePvp: "PVP" togglePvpLower: "PVP" toggleChat: "Chat" toggleChatLower: "chat" -toggleMenuExplosionsName: "Toggle explosions" -# 0 - The toggle status -toggleMenuExplosionsLore: "Explosions are {0}" -toggleMenuFluidSpreadName: "Toggle fluid spread" -# 0 - The toggle status -toggleMenuFluidSpreadLore: "Fluid spread is {0}" -toggleMenuDropsName: "Toggle drops" -# 0 - The toggle status -toggleMenuDropsLore: "Drops are {0}" -toggleMenuRedstoneName: "Redstone" -# 0 - The toggle status -toggleMenuRedstoneLore: "Redstone is {0}" -toggleMenuPvpName: "PVP" -# 0 - The toggle status -toggleMenuPvpLore: "PVP is {0}" -toggleMenuChatName: "Toggle chat" -# 0 - The toggle status -toggleMenuChatLore: "Chat is currently {0}" specifyLoginMessage: "Please specify a login message." # 0 - The login message setOwnLoginMessage: "Your login message is now:> {0}" @@ -312,27 +294,10 @@ prismRollbackMessage: "Rolled back {0} activities" # 0 - Error message returned from Prism prismRollbackError: "Rollback failed: {0}" prismNoResult: "No activities have been rolled back" -pageNext: "Next Page" -pagePrevious: "Previous Page" -pageClose: "Close" -pageableNotInitialized: "Looks like this inventory was not initialized! Please contact a developer to report this." -pageNotFound: "Could not find a page to open" punishmentMenuTitle: "Punishments" # 0 - The player -punishmentPlayerItem: "{0}" -# 0 - The player punishedPlayerMenuTitle: "Punishments - {0}" punishmentPlayerNotFound: 'This player does not exist. Try doing /punishments \ instead.' -# 0 - The punishment type -punishmentItemTitle: "{0}" -# 0 - The punisher -punishmentItemPunisher: "By: {0}" -# 0 - The issue date -punishmentItemIssued: "Issued: {0}" -# 0 - The expiration date -punishmentItemExpires: "Expire(d/s): {0}" -# 0 - The reason -punishmentItemReason: "Reason: {0}" # 0 - The amount of active bans # 1 - The active ban list activeBansList: "Active Bans ({0}): {1}" @@ -343,8 +308,6 @@ updateUpToDate: "Plex is up to date on the {0} channel." # 1 - The channel updateAvailable: "Plex {0} is available on the {1} channel." updateRunCommand: "Run: /plex update" -# 0 - The channel -updateAlreadyUpToDate: "Plex is already up to date on the {0} channel." # 0 - The file name updateDownloading: "Downloading latest JAR file: {0}" updateDownloaded: "New JAR file downloaded and verified successfully." @@ -354,6 +317,23 @@ updateDirectoryFailed: "Unable to create update directory: {0}" updateDownloadFailed: "Something went wrong while downloading {0}. Please check the log for more information." # 0 - The channel updateMetadataNotFound: "No compatible update is available on the {0} channel." +# 0 - The Plex version +# 1 - The channel +# 2 - The supported Minecraft versions +# 3 - The running Minecraft version +updateRequiresMinecraftVersion: "No Plex build exists for Minecraft {3}. Plex {0} on the {1} channel supports Minecraft: {2}." +# 0 - The Plex version +# 1 - The channel +# 2 - The oldest supported Minecraft version +# 3 - The running Minecraft version +# 4 - The supported Minecraft versions +updateRequiresNewerMinecraft: "Your server is running Minecraft {3}, which is older than any version supported by Plex {0} on the {1} channel (oldest supported: {2}). Supported versions: {4}." +# 0 - The Plex version +# 1 - The channel +# 2 - The newest supported Minecraft version +# 3 - The running Minecraft version +# 4 - The supported Minecraft versions +updateUnsupportedNewerMinecraft: "No Plex build exists yet for Minecraft {3}. Plex {0} on the {1} channel supports up to Minecraft {2}. Supported versions: {4}." # 0 - The error message updateMetadataError: "There was an error checking update metadata: {0}" moduleUpdateDisabled: "Skipping {0}; module updates are disabled."