Added an auto updater and a QuickCommandListener combo. Also fixed text bugs.

This commit is contained in:
wolf
2025-06-28 19:50:40 -04:00
parent f08f7bae77
commit d52dd511df
12 changed files with 328 additions and 157 deletions

View File

@@ -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 {

View File

@@ -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<? extends JavaPlugin> getHost() {
return host;
}

View File

@@ -12,15 +12,17 @@ public class Common {
private String flatPrefix;
private boolean flat;
private boolean debugMode;
private final String updateURL;
private final Set<String> 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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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());
});
}
}

View File

@@ -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);
}
}

View File

@@ -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<Component> wrapComponent(Component component, int maxLineLength, int firstLineOffset) {
List<Component> lines = new ArrayList<>();
List<ComponentWord> 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<ComponentWord> 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<TextColor> colors = new HashSet<>();
collectColors(component,colors);
return colors.size() > 1;
return colors.size() <= 1;
}
/**

View File

@@ -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));
}

View File

@@ -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<State> 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<Block> 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<Block> getCurrentBlocks() {
lock.lock();
try {
if (currentIndex >= 0 && currentIndex < history.size()) {
return history.get(currentIndex).getBlocks();
}
return Collections.emptySet();
} finally {
lock.unlock();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}