From d52dd511dfbd657d37415b1d4f3f4d8a212f6d74 Mon Sep 17 00:00:00 2001 From: wolf Date: Sat, 28 Jun 2025 19:50:40 -0400 Subject: [PATCH] Added an auto updater and a QuickCommandListener combo. Also fixed text bugs. --- build.gradle | 19 +-- src/main/java/me/trouper/alias/Alias.java | 12 +- .../java/me/trouper/alias/data/Common.java | 7 +- .../alias/server/commands/QuickCommand.java | 11 +- .../server/commands/QuickCommandListener.java | 16 +++ .../alias/server/events/GuiListener.java | 28 +++-- .../alias/server/events/QuickListener.java | 10 +- .../me/trouper/alias/server/systems/Text.java | 51 ++++++-- .../alias/server/systems/gui/QuickGui.java | 15 ++- .../server/systems/world/BlockHistory.java | 112 ------------------ .../me/trouper/alias/update/AutoUpdater.java | 111 +++++++++++++++++ .../me/trouper/alias/update/UpdateUtils.java | 93 +++++++++++++++ 12 files changed, 328 insertions(+), 157 deletions(-) create mode 100644 src/main/java/me/trouper/alias/server/commands/QuickCommandListener.java delete mode 100644 src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java create mode 100644 src/main/java/me/trouper/alias/update/AutoUpdater.java create mode 100644 src/main/java/me/trouper/alias/update/UpdateUtils.java diff --git a/build.gradle b/build.gradle index 0694b62..00334c6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,5 @@ plugins { id 'java' - id("xyz.jpenilla.run-paper") version "2.3.1" id 'maven-publish' } @@ -23,12 +22,6 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") } -tasks { - runServer { - minecraftVersion("1.21.5") - } -} - def targetJavaVersion = 21 java { def javaVersion = JavaVersion.toVersion(targetJavaVersion) @@ -47,13 +40,9 @@ tasks.withType(JavaCompile).configureEach { } } -processResources { - def props = [version: version] - inputs.properties props - filteringCharset 'UTF-8' - filesMatching('plugin.yml') { - expand props - } +task sourcesJar(type: Jar) { + archiveClassifier.set('sources') + from sourceSets.main.allSource } publishing { @@ -63,6 +52,8 @@ publishing { groupId = project.group artifactId = 'alias' version = project.version + + artifact sourcesJar } } repositories { diff --git a/src/main/java/me/trouper/alias/Alias.java b/src/main/java/me/trouper/alias/Alias.java index d3bc594..f221e1a 100644 --- a/src/main/java/me/trouper/alias/Alias.java +++ b/src/main/java/me/trouper/alias/Alias.java @@ -5,7 +5,7 @@ import me.trouper.alias.server.AutoRegistrar; import me.trouper.alias.server.commands.QuickCommand; import me.trouper.alias.server.events.GuiListener; import me.trouper.alias.server.events.QuickListener; -import me.trouper.alias.server.systems.AbstractWand; +import me.trouper.alias.update.AutoUpdater; import org.bukkit.plugin.java.JavaPlugin; public final class Alias extends JavaPlugin { @@ -21,13 +21,21 @@ public final class Alias extends JavaPlugin { Alias.host = plugin.getClass(); Alias.common = common; - new GuiListener().register(); + AutoUpdater.checkUpdate(plugin,common); + autoRegistrar = new AutoRegistrar(plugin); + autoRegistrar.getQuickListeners().add(new GuiListener()); autoRegistrar.loadAll(common.getPackageName()); enabled = true; } + public static synchronized void stop(JavaPlugin plugin, Common common) { + autoRegistrar.getQuickListeners().forEach(QuickListener::unregister); + autoRegistrar.getQuickCommands().forEach(QuickCommand::disable); + AutoUpdater.checkUpdate(plugin,common); + } + public static Class getHost() { return host; } diff --git a/src/main/java/me/trouper/alias/data/Common.java b/src/main/java/me/trouper/alias/data/Common.java index e9f12d1..2373229 100644 --- a/src/main/java/me/trouper/alias/data/Common.java +++ b/src/main/java/me/trouper/alias/data/Common.java @@ -12,15 +12,17 @@ public class Common { private String flatPrefix; private boolean flat; private boolean debugMode; + private final String updateURL; private final Set debuggerExclusions; - public Common(String packageName, int mainColor, int secondaryColor, String pluginName, String flatPrefix, boolean flat) { + public Common(String packageName, int mainColor, int secondaryColor, String pluginName, String flatPrefix, boolean flat, String updateURL) { this.packageName = packageName; this.mainColor = mainColor; this.secondaryColor = secondaryColor; this.pluginName = pluginName; this.flatPrefix = flatPrefix; this.flat = flat; + this.updateURL = updateURL; this.debugMode = false; this.debuggerExclusions = new HashSet<>(); } @@ -94,4 +96,7 @@ public class Common { } + public String getUpdateURL() { + return updateURL; + } } diff --git a/src/main/java/me/trouper/alias/server/commands/QuickCommand.java b/src/main/java/me/trouper/alias/server/commands/QuickCommand.java index cfd4021..dd0f81c 100644 --- a/src/main/java/me/trouper/alias/server/commands/QuickCommand.java +++ b/src/main/java/me/trouper/alias/server/commands/QuickCommand.java @@ -6,7 +6,6 @@ import me.trouper.alias.server.commands.completions.CompletionNode; import net.kyori.adventure.text.Component; import org.bukkit.command.*; import org.bukkit.entity.Player; -import org.checkerframework.checker.units.qual.A; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -108,4 +107,14 @@ public interface QuickCommand extends TabExecutor, Main { default CommandRegistry getRegistry() { return this.getClass().getAnnotation(CommandRegistry.class); } + + default void disable() { + CommandRegistry registry = this.getClass().getAnnotation(CommandRegistry.class); + PluginCommand command = getPlugin().getCommand(registry.value()); + + if (command != null) { + command.setExecutor((sender, command1, label, args) -> true); + command.setTabCompleter((sender, command2, label, args) -> List.of()); + } + } } diff --git a/src/main/java/me/trouper/alias/server/commands/QuickCommandListener.java b/src/main/java/me/trouper/alias/server/commands/QuickCommandListener.java new file mode 100644 index 0000000..f3390e3 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/commands/QuickCommandListener.java @@ -0,0 +1,16 @@ +package me.trouper.alias.server.commands; + +import me.trouper.alias.server.events.QuickListener; + +public interface QuickCommandListener extends QuickCommand, QuickListener { + @Override + default void register() { + QuickCommand.super.register(); + QuickListener.super.register(); + } + + @Override + default void disable() { + QuickListener.super.unregister(); + } +} diff --git a/src/main/java/me/trouper/alias/server/events/GuiListener.java b/src/main/java/me/trouper/alias/server/events/GuiListener.java index b562337..6953b93 100644 --- a/src/main/java/me/trouper/alias/server/events/GuiListener.java +++ b/src/main/java/me/trouper/alias/server/events/GuiListener.java @@ -2,24 +2,34 @@ package me.trouper.alias.server.events; import me.trouper.alias.server.systems.gui.QuickGui; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.PlayerQuitEvent; public class GuiListener implements QuickListener { - @EventHandler - public void onInventoryClick(InventoryClickEvent e) { - QuickGui.handleClick(e); + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryClick(InventoryClickEvent event) { + QuickGui.handleClick(event); } - @EventHandler - public void onInventoryClose(InventoryCloseEvent e) { - QuickGui.handleClose(e); + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryClose(InventoryCloseEvent event) { + QuickGui.handleClose(event); } - @EventHandler - public void onInventoryDrag(InventoryDragEvent e) { - QuickGui.handleDrag(e); + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryDrag(InventoryDragEvent event) { + QuickGui.handleDrag(event); } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + QuickGui.getRegistries().values().forEach(gui -> { + gui.getViewers().remove(event.getPlayer()); + }); + } + } diff --git a/src/main/java/me/trouper/alias/server/events/QuickListener.java b/src/main/java/me/trouper/alias/server/events/QuickListener.java index e83380a..6c6e453 100644 --- a/src/main/java/me/trouper/alias/server/events/QuickListener.java +++ b/src/main/java/me/trouper/alias/server/events/QuickListener.java @@ -2,11 +2,15 @@ package me.trouper.alias.server.events; import me.trouper.alias.server.Main; import org.bukkit.Bukkit; +import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; public interface QuickListener extends Listener, Main { - default QuickListener register() { - Bukkit.getPluginManager().registerEvents(this,this.getPlugin()); - return this; + default void register() { + Bukkit.getPluginManager().registerEvents(this,main.getPlugin()); + } + + default void unregister() { + HandlerList.unregisterAll(this); } } diff --git a/src/main/java/me/trouper/alias/server/systems/Text.java b/src/main/java/me/trouper/alias/server/systems/Text.java index 2d232b8..a3babef 100644 --- a/src/main/java/me/trouper/alias/server/systems/Text.java +++ b/src/main/java/me/trouper/alias/server/systems/Text.java @@ -30,7 +30,15 @@ public class Text implements Main { * @param args Qualified placeholders to color. */ public static void messageAny(Pallet pallet, boolean playSound, Audience audience, String text, Object... args) { - message(pallet, playSound, audience, color(text), Arrays.stream(args).map(object -> Component.text(String.valueOf(object))).toArray(ComponentLike[]::new)); + message( + pallet, + playSound, + audience, + color(text), + Arrays.stream(args) + .map(object -> object instanceof ComponentLike ? (ComponentLike) object : Component.text(String.valueOf(object))) + .toArray(ComponentLike[]::new) + ); } /** @@ -82,7 +90,13 @@ public class Text implements Main { * @return The final component, formatted according to flat/fancy setting. */ public static Component getMessageAny(Pallet pallet, String text, Object... args) { - return getMessage(pallet, color(text), Arrays.stream(args).map(arg -> color(String.valueOf(arg))).toArray(ComponentLike[]::new)); + return getMessage( + pallet, + color(text), + Arrays.stream(args) + .map(object -> object instanceof ComponentLike ? (ComponentLike) object : Component.text(String.valueOf(object))) + .toArray(ComponentLike[]::new) + ); } /** @@ -150,6 +164,7 @@ public class Text implements Main { /** * Wraps a component into multiple lines based on visible character count. * Preserves all Adventure API formatting including colors, decorations, and events. + * Fixed to prevent unwanted spaces before punctuation. * @param component The component to wrap * @param maxLineLength Maximum visible characters per line * @param firstLineOffset Offset for the first line (plugin name length) @@ -157,6 +172,7 @@ public class Text implements Main { */ private static List wrapComponent(Component component, int maxLineLength, int firstLineOffset) { List lines = new ArrayList<>(); + List words = extractWords(component); if (words.isEmpty()) { @@ -166,20 +182,22 @@ public class Text implements Main { Component currentLine = Component.empty(); int currentLineLength = firstLineOffset; - boolean isFirstLine = true; for (int i = 0; i < words.size(); i++) { ComponentWord word = words.get(i); int wordLength = word.visibleLength(); - int spaceNeeded = (currentLine.equals(Component.empty()) ? 0 : 1) + wordLength; + + boolean needsSpace = !currentLine.equals(Component.empty()) && !startsWithPunctuation(word); + int spaceNeeded = (needsSpace ? 1 : 0) + wordLength; if (currentLineLength + spaceNeeded > maxLineLength && !currentLine.equals(Component.empty())) { lines.add(currentLine); currentLine = Component.empty(); currentLineLength = 0; + needsSpace = false; } - if (!currentLine.equals(Component.empty())) { + if (needsSpace) { currentLine = currentLine.append(Component.space()); currentLineLength++; } @@ -195,6 +213,16 @@ public class Text implements Main { return lines; } + /** + * Checks if a word starts with punctuation that shouldn't have a space before it. + * @param word The word to check + * @return true if the word starts with punctuation + */ + private static boolean startsWithPunctuation(ComponentWord word) { + String text = PlainTextComponentSerializer.plainText().serialize(word.component()); + return !text.isEmpty() && ".,!?;:)]}".indexOf(text.charAt(0)) != -1; + } + /** * Extracts words from a component while preserving their formatting. * @param component The component to extract words from @@ -215,16 +243,15 @@ public class Text implements Main { private static void extractWordsRecursive(Component component, Style inheritedStyle, List words) { Style currentStyle = inheritedStyle.merge(component.style()); - if (component instanceof TextComponent textComponent) { String text = textComponent.content(); if (!text.isEmpty()) { + String[] parts = text.split("\\s+"); - String[] textWords = text.split("\\s+"); - for (String word : textWords) { - if (!word.isEmpty()) { - Component wordComponent = Component.text(word).style(currentStyle); - words.add(new ComponentWord(wordComponent, getVisibleLength(word))); + for (String part : parts) { + if (!part.isEmpty()) { + Component partComponent = Component.text(part).style(currentStyle); + words.add(new ComponentWord(partComponent, getVisibleLength(part))); } } } @@ -315,7 +342,7 @@ public class Text implements Main { private static boolean shouldRecolor(Component component) { Set colors = new HashSet<>(); collectColors(component,colors); - return colors.size() > 1; + return colors.size() <= 1; } /** diff --git a/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java b/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java index 9d43c0e..94d9846 100644 --- a/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java +++ b/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java @@ -67,9 +67,7 @@ public class QuickGui implements InventoryHolder, Main { } public static QuickGui register(String id, QuickGui gui) { - if (gui != null && id != null && !id.isEmpty()) { - registry.put(id, gui); - } + registry.put(id, gui); return gui; } @@ -381,6 +379,17 @@ public class QuickGui implements InventoryHolder, Main { return item(slot, item, action); } + + public GuiBuilder fillSlots(ItemStack item, GuiAction action, int... slots) { + for (int slot : slots) { + if (slot >= 0 && slot < 54 && item != null) { + slotItems.put(slot,item); + slotActions.put(slot,action); + } + } + return this; + } + public GuiBuilder fillBorder(Material material) { return fillBorder(new ItemStack(material)); } diff --git a/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java b/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java deleted file mode 100644 index d4a0ae8..0000000 --- a/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java +++ /dev/null @@ -1,112 +0,0 @@ -package me.trouper.alias.server.systems.world; - -import org.bukkit.block.Block; - -import java.util.*; -import java.util.concurrent.locks.ReentrantLock; - -public class BlockHistory { - - private final List history = new ArrayList<>(); - private final ReentrantLock lock = new ReentrantLock(); - private int currentIndex = -1; - - public void addChange(State state) { - lock.lock(); - try { - if (currentIndex < history.size() - 1) { - history.subList(currentIndex + 1, history.size()).clear(); - } - history.add(state); - currentIndex = history.size() - 1; - } finally { - lock.unlock(); - } - } - - public void addChange(Collection blocks) { - lock.lock(); - try { - if (currentIndex < history.size() - 1) { - history.subList(currentIndex + 1, history.size()).clear(); - } - State newState = new State(blocks); - history.add(newState); - currentIndex = history.size() - 1; - } finally { - lock.unlock(); - } - } - - public boolean canUndo() { - lock.lock(); - try { - return currentIndex > 0; - } finally { - lock.unlock(); - } - } - - public boolean canRedo() { - lock.lock(); - try { - return currentIndex < history.size() - 1; - } finally { - lock.unlock(); - } - } - - public boolean undo() { - lock.lock(); - try { - if (!canUndo()) return false; - currentIndex--; - history.get(currentIndex).restore(); - return true; - } finally { - lock.unlock(); - } - } - - public boolean redo() { - lock.lock(); - try { - if (!canRedo()) return false; - currentIndex++; - history.get(currentIndex).restore(); - return true; - } finally { - lock.unlock(); - } - } - - public int getCurrentIndex() { - lock.lock(); - try { - return currentIndex; - } finally { - lock.unlock(); - } - } - - public int getHistorySize() { - lock.lock(); - try { - return history.size(); - } finally { - lock.unlock(); - } - } - - public Set getCurrentBlocks() { - lock.lock(); - try { - if (currentIndex >= 0 && currentIndex < history.size()) { - return history.get(currentIndex).getBlocks(); - } - return Collections.emptySet(); - } finally { - lock.unlock(); - } - } -} diff --git a/src/main/java/me/trouper/alias/update/AutoUpdater.java b/src/main/java/me/trouper/alias/update/AutoUpdater.java new file mode 100644 index 0000000..7e539dc --- /dev/null +++ b/src/main/java/me/trouper/alias/update/AutoUpdater.java @@ -0,0 +1,111 @@ +package me.trouper.alias.update; + +import me.trouper.alias.Alias; +import me.trouper.alias.data.Common; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; + +public class AutoUpdater { + + public static boolean checkUpdate(JavaPlugin plugin, Common common) { + try { + if (UpdateUtils.isDevelopmentEnvironment(plugin)) { + plugin.getLogger().info("Development environment detected, bypassing update check."); + return false; + } + + String updateURL = common.getUpdateURL(); + if (updateURL == null || updateURL.isEmpty()) { + plugin.getLogger().warning("Update URL is not set."); + return false; + } + + File updateDir = new File("plugins/update"); + if (!updateDir.exists()) updateDir.mkdirs(); + + File currentFile = UpdateUtils.findPluginJar(plugin); + if (currentFile == null) { + plugin.getLogger().severe("Could not locate plugin file in plugins folder."); + return false; + } + + byte[] currentHash = UpdateUtils.getFileHash(currentFile); + String remoteHashURL = updateURL.replace("/download/", "/hash/sha256/"); + + String remoteHashHex = UpdateUtils.fetchRemoteHash(remoteHashURL); + if (remoteHashHex == null) { + plugin.getLogger().warning("Failed to fetch remote hash from: " + remoteHashURL); + return false; + } + + String currentHashHex = UpdateUtils.bytesToHex(currentHash); + if (remoteHashHex.equalsIgnoreCase(currentHashHex)) { + plugin.getLogger().info("Plugin is up to date."); + return false; + } + + File updateFile = new File(updateDir, currentFile.getName()); + if (updateFile.exists()) { + byte[] updateHash = UpdateUtils.getFileHash(updateFile); + String updateHashHex = UpdateUtils.bytesToHex(updateHash); + + if (remoteHashHex.equalsIgnoreCase(updateHashHex)) { + plugin.getLogger().info("An update is already downloaded and ready."); + return false; + } else { + plugin.getLogger().info("Found outdated update file in plugins/update/. It will be replaced."); + updateFile.delete(); + } + } + + plugin.getLogger().info("Update available. Downloading new version..."); + File downloaded = downloadFile(updateURL); + if (downloaded == null) { + plugin.getLogger().warning("Failed to download update file."); + return false; + } + + File destination = new File(updateDir, currentFile.getName()); + Files.copy(downloaded.toPath(), destination.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info("Saved updated plugin to: " + destination.getAbsolutePath()); + + return true; + + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error during update check", e); + return false; + } + } + + + private static File downloadFile(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "AliasUpdater"); + conn.connect(); + + if (conn.getResponseCode() != 200) return null; + + File tempFile = File.createTempFile("plugin_update_", ".jar"); + try (InputStream in = conn.getInputStream(); FileOutputStream out = new FileOutputStream(tempFile)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + } + return tempFile; + } + + +} diff --git a/src/main/java/me/trouper/alias/update/UpdateUtils.java b/src/main/java/me/trouper/alias/update/UpdateUtils.java new file mode 100644 index 0000000..2fab263 --- /dev/null +++ b/src/main/java/me/trouper/alias/update/UpdateUtils.java @@ -0,0 +1,93 @@ +package me.trouper.alias.update; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class UpdateUtils { + + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + + public static String fetchRemoteHash(String urlStr) { + try { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "AliasUpdater"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + return reader.readLine().trim(); + } + } catch (IOException e) { + return null; + } + } + + public static byte[] getFileHash(File file) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream fis = new FileInputStream(file)) { + byte[] byteArray = new byte[8192]; + int bytesCount; + while ((bytesCount = fis.read(byteArray)) != -1) { + digest.update(byteArray, 0, bytesCount); + } + } + return digest.digest(); + } + + public static File findPluginJar(JavaPlugin plugin) { + File pluginDir = new File("plugins"); + if (!pluginDir.exists() || !pluginDir.isDirectory()) return null; + + String expectedName = plugin.getDescription().getName(); + String expectedMain = plugin.getDescription().getMain(); + + for (File file : pluginDir.listFiles()) { + if (!file.getName().endsWith(".jar")) continue; + + try (JarFile jar = new JarFile(file)) { + JarEntry entry = jar.getJarEntry("plugin.yml"); + if (entry == null) continue; + + try (InputStream is = jar.getInputStream(entry)) { + YamlConfiguration yml = new YamlConfiguration(); + yml.load(new InputStreamReader(is)); + + String name = yml.getString("name"); + String main = yml.getString("main"); + + if (expectedName.equalsIgnoreCase(name) && expectedMain.equalsIgnoreCase(main)) { + return file; + } + + } catch (Exception ignored) {} + } catch (IOException ignored) {} + } + + return null; + } + + + public static boolean isDevelopmentEnvironment(JavaPlugin plugin) { + try { + return "TRUE".equalsIgnoreCase(System.getenv("ALIAS_DEVELOPMENT")); + } catch (Exception e) { + plugin.getLogger().warning("Could not determine runtime environment, assuming production."); + return false; + } + } +}