diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5b063201 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/core/pom.xml b/core/pom.xml index b7d1c976..6ce2db1b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -35,9 +35,9 @@ - org.spigotmc - spigot-api - ${project.cbversion} + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT provided @@ -53,13 +53,13 @@ com.google.code.gson gson - 2.8.5 + 2.12.1 commons-io commons-io - 2.5 + 2.18.0 @@ -81,15 +81,26 @@ org.slf4j slf4j-simple - 1.7.25 + 2.0.17 + test + + + net.kyori + adventure-api + 4.19.0 + + + net.kyori + adventure-platform-bukkit + 4.3.4 - spigot-repo - https://hub.spigotmc.org/nexus/content/groups/public/ + papermc + https://repo.papermc.io/repository/maven-public/ \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/SignManager.java b/core/src/main/java/net/licks92/wirelessredstone/SignManager.java index 943f50ee..7046e9ab 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/SignManager.java +++ b/core/src/main/java/net/licks92/wirelessredstone/SignManager.java @@ -309,7 +309,7 @@ public boolean isWirelessRedstoneSign(Block block) { Sign sign = (Sign) block.getState(); - if (Utils.getSignType(sign.getLine(0)) == null || sign.getLine(1).equalsIgnoreCase("")) { + if (Utils.getType(sign.getLine(0), sign.getLine(2)) == null || sign.getLine(1).equalsIgnoreCase("")) { return false; } diff --git a/core/src/main/java/net/licks92/wirelessredstone/UpdateChecker.java b/core/src/main/java/net/licks92/wirelessredstone/UpdateChecker.java index 84a1517a..34dcc098 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/UpdateChecker.java +++ b/core/src/main/java/net/licks92/wirelessredstone/UpdateChecker.java @@ -1,20 +1,20 @@ package net.licks92.wirelessredstone; -import com.github.zafarkhaja.semver.Version; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; import java.util.stream.IntStream; public class UpdateChecker { @@ -22,120 +22,157 @@ public class UpdateChecker { private static final String USER_AGENT = "WirelessRedstone-update-checker"; private static final String UPDATE_URL = "https://wirelessredstonegroup.github.io/WirelessRedstoneUpdate/update2.json"; - private static UpdateChecker instance; - - private UpdateResult lastResult = null; - + private static volatile UpdateChecker instance; // Ensures thread-safety for singleton private final JavaPlugin plugin; + private volatile UpdateResult lastResult; // Thread-safe storage + private final HttpClient httpClient; // Modern HTTP client for async requests + // Constructor private UpdateChecker(JavaPlugin plugin) { this.plugin = plugin; + this.httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); } + /** + * Initializes the UpdateChecker for the given plugin. + */ public static UpdateChecker init(JavaPlugin plugin) { Objects.requireNonNull(plugin, "Plugin object cannot be NULL"); + if (instance == null) { // Double-checked locking for thread safety + synchronized (UpdateChecker.class) { + if (instance == null) { + instance = new UpdateChecker(plugin); + } + } + } + return instance; + } - return instance != null ? instance : (instance = new UpdateChecker(plugin)); + /** + * Returns the current instance of UpdateChecker. + * + * @throws IllegalStateException if not initialized + */ + public static UpdateChecker getInstance() { + if (instance == null) { + throw new IllegalStateException("UpdateChecker is not initialized!"); + } + return instance; } + /** + * Performs an asynchronous update check. + * + * @return a CompletableFuture that completes with the update result. + */ public CompletableFuture requestUpdateCheck() { return CompletableFuture.supplyAsync(() -> { try { - URL url = new URL(UPDATE_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - connection.addRequestProperty("User-Agent", USER_AGENT); + // Create HTTP request + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(UPDATE_URL)) + .header("User-Agent", USER_AGENT) + .build(); + + // Send request and get response + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + plugin.getLogger().warning("Failed to fetch update info. HTTP Status: " + response.statusCode()); + return new UpdateResult(UpdateReason.COULD_NOT_CONNECT, null, null, null); + } - InputStreamReader reader = new InputStreamReader(connection.getInputStream()); + // Parse the response body + return parseResponse(response.body()); + } catch (IOException | InterruptedException e) { + plugin.getLogger().severe("Failed to check for updates: " + e.getMessage()); + return new UpdateResult(UpdateReason.COULD_NOT_CONNECT, null, null, null); + } catch (JsonSyntaxException e) { + plugin.getLogger().warning("Invalid JSON received from update server."); + return new UpdateResult(UpdateReason.INVALID_JSON, null, null, null); + } + }); + } - JsonElement element = new JsonParser().parse(reader); - if (!element.isJsonObject()) { - return new UpdateResult(UpdateReason.INVALID_JSON); - } + /** + * Parses the JSON response to determine update results. + */ + private UpdateResult parseResponse(String responseBody) { + JsonElement rootElement = JsonParser.parseString(responseBody); + if (!rootElement.isJsonObject()) { + return new UpdateResult(UpdateReason.INVALID_JSON, null, null, null); + } - reader.close(); + JsonObject root = rootElement.getAsJsonObject(); + JsonObject latest = root.getAsJsonObject("latest"); + JsonObject versions = root.getAsJsonObject("versions"); - JsonObject versionObject = element.getAsJsonObject().getAsJsonObject("latest"); - JsonObject versionsObject = element.getAsJsonObject().getAsJsonObject("versions"); + String spigotVersion = latest.get("spigotversion").getAsString(); + String currentVersion = plugin.getDescription().getVersion(); - if (!versionsObject.has(versionObject.get("spigotversion").getAsString())) { - return new UpdateResult(UpdateReason.INVALID_JSON); - } + if (!versions.has(spigotVersion)) { + return new UpdateResult(UpdateReason.INVALID_JSON, null, null, null); + } - JsonObject updateObject = versionsObject.getAsJsonObject(versionObject.get("spigotversion").getAsString()); + JsonObject updateData = versions.getAsJsonObject(spigotVersion); - Version current = Version.valueOf(plugin.getDescription().getVersion()); - Version newest = Version.valueOf(versionObject.get("spigotversion").getAsString()); + boolean isUpdateAvailable = versionGreaterThan(spigotVersion, currentVersion); + if (isUpdateAvailable) { + String downloadUrl = updateData.get("downloadUrl").getAsString(); + List changelog = IntStream.range(0, updateData.getAsJsonArray("changelog").size()) + .mapToObj(i -> updateData.getAsJsonArray("changelog").get(i).getAsString()) + .toList(); - String updateUrl = updateObject.getAsJsonObject("spigot") - .get("downloadUrl").getAsString(); + return new UpdateResult(UpdateReason.NEW_UPDATE, spigotVersion, downloadUrl, changelog); + } - List changelog = IntStream - .range(0, updateObject.getAsJsonArray("changelog").size()) - .mapToObj(i -> updateObject.getAsJsonArray("changelog").get(i).getAsString()) - .collect(Collectors.toList()); + return new UpdateResult(UpdateReason.UP_TO_DATE, currentVersion, null, null); + } - return newest.greaterThan(current) ? new UpdateResult(UpdateReason.NEW_UPDATE, newest.toString(), updateUrl, changelog) : - new UpdateResult(UpdateReason.UP_TO_DATE); - } catch (IOException e) { - e.printStackTrace(); - return new UpdateResult(UpdateReason.COULD_NOT_CONNECT); - } catch (JsonSyntaxException e) { - return new UpdateResult(UpdateReason.INVALID_JSON); - } - }); + /** + * Compares two version strings and determines if the first version is greater than the second. + * The versions are expected to be in a semantic versioning format. + * + * @param newVersion the new version string to compare, must not be null + * @param currentVersion the current version string to compare against, must not be null + * @return true if the new version is greater than the current version; false if not, or if an exception occurs + */ + private boolean versionGreaterThan(@NotNull String newVersion, @NotNull String currentVersion) { + try { + var newVer = com.github.zafarkhaja.semver.Version.valueOf(newVersion); + var currentVer = com.github.zafarkhaja.semver.Version.valueOf(currentVersion); + + return newVer.greaterThan(currentVer); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Invalid version string provided: " + e.getMessage()); + } + return false; } + /** + * Returns the result of the last update check performed by the UpdateChecker. + * + * @return the {@link UpdateResult} object containing information about the update + * check, such as the update reason, newest version, download URL, and changelog. + */ public UpdateResult getLastResult() { return lastResult; } + // Enum for update reason explanations public enum UpdateReason { - NEW_UPDATE, UP_TO_DATE, COULD_NOT_CONNECT, INVALID_JSON; + NEW_UPDATE, UP_TO_DATE, COULD_NOT_CONNECT, INVALID_JSON } - public final class UpdateResult { - private final UpdateReason reason; - private final String newestVersion, url; - private List changelog; - - { - UpdateChecker.this.lastResult = this; - } - - private UpdateResult(UpdateReason reason, String newestVersion, String url, List changelog) { - this.reason = reason; - this.newestVersion = newestVersion; - this.url = url; - this.changelog = changelog; - } - - private UpdateResult(UpdateReason reason) { - if (reason == UpdateReason.NEW_UPDATE) { - throw new IllegalArgumentException("Reasons that require updates must also provide the latest version, URL and changelog"); - } - - this.reason = reason; - this.newestVersion = plugin.getDescription().getVersion(); - this.url = null; - this.changelog = null; - } + /** + * Record to represent the result of an update check. + */ + public record UpdateResult(UpdateReason reason, String newestVersion, + String downloadUrl, List changelog) { public boolean updateAvailable() { - return this.reason == UpdateReason.NEW_UPDATE; - } - - public String getNewestVersion() { - return newestVersion; - } - - public String getUrl() { - return url; - } - - public List getChangelog() { - return changelog; + return reason == UpdateChecker.UpdateReason.NEW_UPDATE; } @Override @@ -143,9 +180,17 @@ public String toString() { return "UpdateResult{" + "reason=" + reason + ", newestVersion='" + newestVersion + '\'' + - ", url='" + url + '\'' + + ", downloadUrl='" + downloadUrl + '\'' + ", changelog=" + changelog + '}'; } + + public String getNewestVersion() { + return newestVersion; + } + + public String getUrl() { + return downloadUrl; + } } -} +} \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/Utils.java b/core/src/main/java/net/licks92/wirelessredstone/Utils.java index 06c786f9..255de506 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/Utils.java +++ b/core/src/main/java/net/licks92/wirelessredstone/Utils.java @@ -6,333 +6,267 @@ import org.bukkit.Location; import org.bukkit.block.BlockFace; import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; -import java.util.Arrays; -import java.util.Collection; +import java.util.*; public class Utils { - private static final BlockFace[] axis = {BlockFace.SOUTH, BlockFace.WEST, BlockFace.NORTH, BlockFace.EAST}; - private static final BlockFace[] fullAxis = {BlockFace.SOUTH, BlockFace.WEST, BlockFace.NORTH, BlockFace.EAST, BlockFace.UP, BlockFace.DOWN}; - + private static final List AXIS = List.of(BlockFace.SOUTH, BlockFace.WEST, BlockFace.NORTH, BlockFace.EAST); + private static final List FULL_AXIS = List.of( + BlockFace.SOUTH, BlockFace.WEST, BlockFace.NORTH, BlockFace.EAST, + BlockFace.UP, BlockFace.DOWN); + + private static final Map WALL_DIRECTIONS = Map.of( + 2, BlockFace.NORTH, 3, BlockFace.SOUTH, 4, BlockFace.WEST, 5, BlockFace.EAST + ); + + private static final Map FLOOR_DIRECTIONS = Map.ofEntries( + Map.entry(0, BlockFace.SOUTH), Map.entry(1, BlockFace.SOUTH_SOUTH_WEST), + Map.entry(2, BlockFace.SOUTH_WEST), Map.entry(3, BlockFace.WEST_SOUTH_WEST), + Map.entry(4, BlockFace.WEST), Map.entry(5, BlockFace.WEST_NORTH_WEST), + Map.entry(6, BlockFace.NORTH_WEST), Map.entry(7, BlockFace.NORTH_NORTH_WEST), + Map.entry(8, BlockFace.NORTH), Map.entry(9, BlockFace.NORTH_NORTH_EAST), + Map.entry(10, BlockFace.NORTH_EAST), Map.entry(11, BlockFace.EAST_NORTH_EAST), + Map.entry(12, BlockFace.EAST), Map.entry(13, BlockFace.EAST_SOUTH_EAST), + Map.entry(14, BlockFace.SOUTH_EAST), Map.entry(15, BlockFace.SOUTH_SOUTH_EAST) + ); /** - * This checks if the current Minecraft server version is compatible with WirelessRedstone. + * Retrieves the current Minecraft version as a string using the Bukkit API. * - * @return If the plugin is compatible + * @return A string representing the current Minecraft version. */ - public static boolean isCompatible() { - final String packageName = Bukkit.getServer().getClass().getPackage().getName(); - String bukkitVersion = packageName.substring(packageName.lastIndexOf('.') + 1); - - try { - String[] pieces = bukkitVersion.substring(1).split("_"); - - return Integer.parseInt(pieces[0]) >= 1 && Integer.parseInt(pieces[1]) >= 8; - } catch (NumberFormatException | NullPointerException e) { - return false; - } + private static String getMinecraftVersion() { + // Utilize Paper's API for better version handling + return Bukkit.getMinecraftVersion(); } /** - * This checks if the new material system is in place. + * Parses the current Minecraft version string into a {@code Version} object. + * Extracts the major and minor version numbers from the string and + * encapsulates them into a {@code Version} record. If parsing the version + * fails, an empty {@code Optional} is returned. * - * @return If the new material system is needed + * @return An {@code Optional} containing the parsed version if successful, + * or an empty {@code Optional} if an exception occurs during parsing. */ - public static boolean isNewMaterialSystem() { - final String packageName = Bukkit.getServer().getClass().getPackage().getName(); - String bukkitVersion = packageName.substring(packageName.lastIndexOf('.') + 1); - + private static Optional parseVersion() { try { - String[] pieces = bukkitVersion.substring(1).split("_"); - - return Integer.parseInt(pieces[0]) >= 1 && Integer.parseInt(pieces[1]) >= 13; - } catch (NumberFormatException | NullPointerException e) { - return false; + String version = getMinecraftVersion(); + String[] parts = version.split("\\."); + return Optional.of(new Version(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]))); + } catch (Exception e) { + return Optional.empty(); } } /** - * Display a message with prefix to a specific user. This ignores the silent mode. + * Determines if the current Minecraft version is compatible by checking + * whether the major version is greater than 1, or if the major version is 1 + * and the minor version is at least 8. * - * @param message Text message - * @param sender Where send the message to - * @param error Is the message an error + * @return true if the Minecraft version is 1.8 or newer, false if parsing fails or the version is incompatible. */ - public static void sendFeedback(String message, CommandSender sender, boolean error) { - sendFeedback(message, sender, error, false); + public static boolean isCompatible() { + return parseVersion() + .map(version -> version.major() > 1 || (version.major() == 1 && version.minor() >= 8)) + .orElse(false); } /** - * Display a message with prefix to a specific user. + * Determines if the current system uses the new Minecraft material system + * introduced in version 1.13 or higher. * - * @param message Text message - * @param sender Where send the message to - * @param error Is the message an error - * @param checkSilent Don't display message if silent mode is on + * @return true if the Minecraft version is 1.13 or newer, false otherwise. */ - public static void sendFeedback(String message, CommandSender sender, boolean error, boolean checkSilent) { - if (ConfigManager.getConfig().getSilentMode() && checkSilent) - return; - sender.sendMessage(ChatColor.GRAY + "[" + ChatColor.RED + "WirelessRedstone" + ChatColor.GRAY + "] " - + (error ? ChatColor.RED : ChatColor.GREEN) + message); + public static boolean isNewMaterialSystem() { + return parseVersion() + .map(version -> version.major() > 1 || (version.major() == 1 && version.minor() >= 13)) + .orElse(false); } /** - * Display a message to a specific user. This ignores the silent mode. + * Sends a message to the specified {@code CommandSender} with an optional prefix and + * formatting to indicate whether it is an error message. The message can be + * conditionally suppressed based on a silent mode configuration. * - * @param message Text message - * @param sender Where send the message to - * @param error Is the message an error + * @param message The message to be sent to the {@code CommandSender}. + * @param sender The {@code CommandSender} who will receive the message. + * @param isError Whether the message represents an error. If {@code true}, + * the message is displayed in red; otherwise, it is displayed in green. + * @param prefix An optional prefix string to prepend to the message. If + * null, no prefix is added. + * @param checkSilent Whether to check for silent mode configuration. If {@code true}, + * the message will not be sent if silent mode is enabled. */ - public static void sendCommandFeedback(String message, CommandSender sender, boolean error) { - sendCommandFeedback(message, sender, error, false); + private static void sendMessageToSender(String message, CommandSender sender, boolean isError, String prefix, boolean checkSilent) { + if (checkSilent && ConfigManager.getConfig().getSilentMode()) return; + + String formattedMessage = Optional.ofNullable(prefix) + .map(p -> ChatColor.GRAY + "[" + ChatColor.RED + p + ChatColor.GRAY + "] ") + .orElse("") + + (isError ? ChatColor.RED : ChatColor.GREEN) + message; + + sender.sendMessage(formattedMessage); } - /** - * Display a message to a specific user. - * - * @param message Text message - * @param sender Where send the message to - * @param error Is the message an error - * @param checkSilent Don't display message if silent mode is on - */ - public static void sendCommandFeedback(String message, CommandSender sender, boolean error, boolean checkSilent) { - if (ConfigManager.getConfig().getSilentMode() && checkSilent) - return; - sender.sendMessage((error ? ChatColor.RED : ChatColor.GREEN) + message); + public static void sendFeedback(String message, CommandSender sender, boolean isError) { + sendMessageToSender(message, sender, isError, "WirelessRedstone", false); } - /** - * Converts the old direction system to the new BlockFace system. - * - * @param isWallSign Is the sign against a wall - * @param direction Old sign facing id system - * @return BlockFace - */ - public static BlockFace getBlockFace(boolean isWallSign, int direction) { - BlockFace blockFace; + public static void sendFeedback(String message, CommandSender sender, boolean isError, boolean checkSilent) { + sendMessageToSender(message, sender, isError, "WirelessRedstone", checkSilent); + } - if (isWallSign) { - if (direction == 2) { - blockFace = BlockFace.NORTH; - } else if (direction == 3) { - blockFace = BlockFace.SOUTH; - } else if (direction == 4) { - blockFace = BlockFace.WEST; - } else if (direction == 5) { - blockFace = BlockFace.EAST; - } else { - blockFace = BlockFace.NORTH; - } - } else { - if (direction == 0) { - blockFace = BlockFace.SOUTH; - } else if (direction == 1) { - blockFace = BlockFace.SOUTH_SOUTH_WEST; - } else if (direction == 2) { - blockFace = BlockFace.SOUTH_WEST; - } else if (direction == 3) { - blockFace = BlockFace.WEST_SOUTH_WEST; - } else if (direction == 4) { - blockFace = BlockFace.WEST; - } else if (direction == 5) { - blockFace = BlockFace.WEST_NORTH_WEST; - } else if (direction == 6) { - blockFace = BlockFace.NORTH_WEST; - } else if (direction == 7) { - blockFace = BlockFace.NORTH_NORTH_WEST; - } else if (direction == 8) { - blockFace = BlockFace.NORTH; - } else if (direction == 9) { - blockFace = BlockFace.NORTH_NORTH_EAST; - } else if (direction == 10) { - blockFace = BlockFace.NORTH_EAST; - } else if (direction == 11) { - blockFace = BlockFace.EAST_NORTH_EAST; - } else if (direction == 12) { - blockFace = BlockFace.EAST; - } else if (direction == 13) { - blockFace = BlockFace.EAST_SOUTH_EAST; - } else if (direction == 14) { - blockFace = BlockFace.SOUTH_EAST; - } else if (direction == 15) { - blockFace = BlockFace.SOUTH_SOUTH_EAST; - } else { - blockFace = BlockFace.SOUTH; - } - } + public static void sendCommandFeedback(String message, CommandSender sender, boolean isError) { + sendMessageToSender(message, sender, isError, null, false); + } - return blockFace; + public static void sendCommandFeedback(String message, CommandSender sender, boolean isError, boolean checkSilent) { + sendMessageToSender(message, sender, isError, null, checkSilent); } /** - * Deprecated!
- * Converts BlockFace to a raw byte direction for wall signs/torches. + * Determines the appropriate BlockFace direction based on whether the block is a wall sign + * or a floor-mounted block, and the provided direction value. * - * @param isTorch If the block is a torch - * @param blockFace The direction the wall sign/torch is facing - * @return raw byte code for direction + * @param isWallSign Indicates if the block is a wall sign. If true, the direction will be + * determined using the wall directions map. If false, the floor directions map + * will be used. + * @param direction The integer value representing the direction. This value is used as a key + * to determine the corresponding BlockFace. + * @return The corresponding BlockFace for the given block type and direction value. Defaults + * to BlockFace.NORTH for wall signs and BlockFace.SOUTH for floor blocks if the + * direction is not found in the respective map. */ - @Deprecated - public static byte getRawData(boolean isTorch, BlockFace blockFace) { - if (isTorch) { - if (blockFace == BlockFace.NORTH) - return (byte)4; - else if (blockFace == BlockFace.SOUTH) - return (byte)3; - else if (blockFace == BlockFace.WEST) - return (byte)2; - else if (blockFace == BlockFace.EAST) - return (byte)1; - return (byte)0; - } else { - if (blockFace == BlockFace.NORTH) - return (byte)2; - else if (blockFace == BlockFace.SOUTH) - return (byte)3; - else if (blockFace == BlockFace.WEST) - return (byte)4; - else if (blockFace == BlockFace.EAST) - return (byte)5; - return (byte)0; - } + public static BlockFace getBlockFace(boolean isWallSign, int direction) { + return isWallSign + ? WALL_DIRECTIONS.getOrDefault(direction, BlockFace.NORTH) + : FLOOR_DIRECTIONS.getOrDefault(direction, BlockFace.SOUTH); } /** - * Gives a collection of adjacent BlockFaces. + * Generates raw data byte information based on the state of a torch and the specified BlockFace direction. * - * @return All the possible adjacent BlockFaces + * @param isTorch Indicates whether the object is a torch. + * @param blockFace The direction represented by a BlockFace enum. + * @return A byte value representing the raw data for the given torch state and BlockFace. + * @deprecated This method is deprecated and may be removed in future updates. */ - public static Collection getAxisBlockFaces() { - return getAxisBlockFaces(true); + @Deprecated + public static byte getRawData(boolean isTorch, BlockFace blockFace) { + return switch (blockFace) { + case NORTH -> (byte) (isTorch ? 4 : 2); + case SOUTH -> (byte) (isTorch ? 3 : 3); + case WEST -> (byte) (isTorch ? 2 : 4); + case EAST -> (byte) (isTorch ? 1 : 5); + default -> (byte) 0; + }; } /** - * Gives a collection of adjacent BlockFaces. + * Generates a Minecraft-specific teleportation command string to be used with a player. * - * @param upAndDown Include directions up and down - * @return All the possible adjacent BlockFaces + * @param playerName The name of the player for which the teleportation string is generated. + * @return A formatted string representing the teleportation command based on the player's name. */ - public static Collection getAxisBlockFaces(boolean upAndDown) { - return Arrays.asList(upAndDown ? fullAxis : axis); + public static String getTeleportString(String playerName) { + return """ + tellraw %s [ + "", + {"text": "[", "color": "gray", + "clickEvent": {"action": "run_command", "value": "%%COMMAND"}, + "hoverEvent": {"action": "show_text", "value": {"text": "", "extra": [{"text": "%%HOVERTEXT"}]}} + }, + {"text": "\\u27A4", "color": "aqua", "bold": true, + "clickEvent": {"action": "run_command", "value": "%%COMMAND"}, + "hoverEvent": {"action": "show_text", "value": {"text": "", "extra": [{"text": "%%HOVERTEXT"}]}} + }, + {"text": "] ", "color": "gray", + "clickEvent": {"action": "run_command", "value": "%%COMMAND"}, + "hoverEvent": {"action": "show_text", "value": {"text": "", "extra": [{"text": "%%HOVERTEXT"}]}} + }, + {"text": "Name %%NAME, type: %%TYPE, world: %%WORLD, x: %%XCOORD, y: %%YCOORD, z: %%ZCOORD", + "color": "green", + "clickEvent": {"action": "run_command", "value": "%%COMMAND"}, + "hoverEvent": {"action": "show_text", "value": {"text": "", "extra": [{"text": "%%HOVERTEXT"}]}} + } + ] + """.formatted(playerName); } /** - * Gets the horizontal Block Face from a given yaw angle
+ * Retrieves a collection of BlockFace directions that align with an axis. + * Optionally includes the up and down BlockFaces depending on the parameter. * - * @param yaw angle - * @return The Block Face of the angle + * @param includeUpAndDown If true, the resulting collection includes both up and down directions; + * otherwise, only horizontal axis directions are included. + * @return A collection of BlockFace directions based on the specified parameter. */ - public static BlockFace yawToFace(float yaw) { - return axis[Math.round(yaw / 90f) & 0x3]; - } - - public static SignType getType(String text) { - switch (text.toUpperCase()) { - case "TRANSMITTERS": - case "TRANSMITTER": - case "T": - return SignType.TRANSMITTER; - case "RECEIVERS": - case "RECEIVER": - case "R": - return SignType.RECEIVER; - case "SCREENS": - case "SCREEN": - case "S": - return SignType.SCREEN; - case "INVERTERS": - case "INVERTER": - case "INVERT": - case "I": - return SignType.RECEIVER_INVERTER; - case "SWITCHERS": - case "SWITCHER": - case "SWITCHS": - case "SWITCH": - return SignType.RECEIVER_SWITCH; - case "CLOCKS": - case "CLOCK": - case "C": - return SignType.RECEIVER_CLOCK; - case "DELAYERS": - case "DELAYER": - case "DELAY": - case "D": - return SignType.RECEIVER_DELAYER; - } - - return null; + public static Collection getAxisBlockFaces(boolean includeUpAndDown) { + return includeUpAndDown ? FULL_AXIS : AXIS; } /** - * Returns a SignType based on the first line of a sign.
- * All receiver types are returned as SignType.RECEIVER + * Retrieves a collection of BlockFace values that represent the primary axis directions. * - * @param firstLine First line of a sign - * @return SignType + * @return A collection of BlockFace values corresponding to the primary axis directions. */ - public static SignType getSignType(String firstLine) { - return getSignType(firstLine, ""); + public static Collection getAxisBlockFaces() { + return FULL_AXIS; } /** - * Returns a SignType based on the first and third line of a sign.
- * This returns a specific receiver type. + * Converts a yaw angle into a corresponding BlockFace direction. * - * @param firstLine First line of a sign - * @param secondLine Third line of a sign - * @return SignType + * @param yaw The yaw angle to be converted, measured in degrees. Typically ranges from -180 to 180. + * @return The BlockFace that corresponds to the given yaw angle. */ - public static SignType getSignType(String firstLine, String secondLine) { - if (WirelessRedstone.getStringManager().tagsTransmitter.contains(firstLine.toLowerCase())) { - return SignType.TRANSMITTER; - } else if (WirelessRedstone.getStringManager().tagsScreen.contains(firstLine.toLowerCase())) { - return SignType.SCREEN; - } else if (WirelessRedstone.getStringManager().tagsReceiver.contains(firstLine.toLowerCase())) { - if (WirelessRedstone.getStringManager().tagsReceiverInverterType.contains(secondLine.toLowerCase())) { - return SignType.RECEIVER_INVERTER; - } else if (WirelessRedstone.getStringManager().tagsReceiverSwitchType.contains(secondLine.toLowerCase())) { - return SignType.RECEIVER_SWITCH; - } else if (WirelessRedstone.getStringManager().tagsReceiverClockType.contains(secondLine.toLowerCase())) { - return SignType.RECEIVER_CLOCK; - } else if (WirelessRedstone.getStringManager().tagsReceiverDelayerType.contains(secondLine.toLowerCase())) { - return SignType.RECEIVER_DELAYER; - } - - return SignType.RECEIVER; - } - - return null; + public static BlockFace yawToFace(float yaw) { + return AXIS.get(Math.round(yaw / 90) & 0x3); } /** - * Check if two locations are in the same place. + * Compares two Location objects to determine if they represent the same block location in the same world. * - * @param loc1 Location - * @param loc2 Location - * @return Boolean + * @param loc1 The first Location object to compare, may be null. + * @param loc2 The second Location object to compare, may be null. + * @return True if both locations share the same block coordinates (x, y, z) and world name, otherwise false. */ public static boolean sameLocation(Location loc1, Location loc2) { - if (loc1 == null || loc2 == null) { - return false; - } else if (loc1.getWorld() == null || loc2.getWorld() == null) { + if (loc1 == null || loc2 == null || loc1.getWorld() == null || loc2.getWorld() == null) { return false; } - - return loc1.getBlockX() == loc2.getBlockX() && loc1.getBlockY() == loc2.getBlockY() && loc1.getBlockZ() == loc2.getBlockZ() && + return loc1.getBlockX() == loc2.getBlockX() && + loc1.getBlockY() == loc2.getBlockY() && + loc1.getBlockZ() == loc2.getBlockZ() && loc1.getWorld().getName().equalsIgnoreCase(loc2.getWorld().getName()); } /** - * Mix teleport command together with a player name. + * Determines the SignType based on the provided text and line. * - * @param playerName Player name - * @return Command + * @param text The text to be evaluated, indicating the type of sign. + * Acceptable values include specific keywords like "TRANSMITTER", "RECEIVER", etc. + * @param line The additional line parameter, which must not be null. + * @return The corresponding SignType if a match is found, otherwise null. */ - public static String getTeleportString(String playerName) { - return "tellraw " + playerName + " " + "[\"\",{\"text\":\"[\",\"color\":\"gray\",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"%%COMMAND\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"%%HOVERTEXT\"}]}}},{\"text\":\"\\u27A4\",\"color\":\"aqua\",\"bold\":true,\"clickEvent\":{\"action\":\"run_command\",\"value\":\"%%COMMAND\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"%%HOVERTEXT\"}]}}},{\"text\":\"] \",\"color\":\"gray\",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"%%COMMAND\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"%%HOVERTEXT\"}]}},\"bold\":false},{\"text\":\"Name %%NAME, type: %%TYPE, world: %%WORLD, x: %%XCOORD, y: %%YCOORD, z: %%ZCOORD\",\"color\":\"green\",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"%%COMMAND\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":{\"text\":\"\",\"extra\":[{\"text\":\"%%HOVERTEXT\"}]}}}]"; + public static SignType getType(String text, @NotNull String line) { + return switch (text.toUpperCase()) { + case "TRANSMITTER", "T" -> SignType.TRANSMITTER; + case "RECEIVER", "R" -> SignType.RECEIVER; + case "SCREEN", "S" -> SignType.SCREEN; + case "INVERTER", "I" -> SignType.RECEIVER_INVERTER; + case "SWITCHER" -> SignType.RECEIVER_SWITCH; + case "CLOCK", "C" -> SignType.RECEIVER_CLOCK; + case "DELAYER", "D" -> SignType.RECEIVER_DELAYER; + default -> null; + }; + } + + public static void sendFeedback(String s, CommandSender sender, boolean b, Object o, boolean b1) { } -} + private record Version(int major, int minor) {} +} \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/WRLogger.java b/core/src/main/java/net/licks92/wirelessredstone/WRLogger.java index 2779498b..29ebfe4a 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/WRLogger.java +++ b/core/src/main/java/net/licks92/wirelessredstone/WRLogger.java @@ -1,72 +1,122 @@ package net.licks92.wirelessredstone; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.ConsoleCommandSender; public class WRLogger { - private final ConsoleCommandSender console; - private final String prefix; + private final Audience audience; + private final Component prefixComponent; private final boolean debug; private final boolean color; /** * Creates an instance of WRLogger. * - * @param prefix This is added before all messages - * @param console Console reference from bukkit/spigot - * @param debug Enable debug mode - * @param color Enable color messages + * @param prefix This is added before all messages. + * @param console ConsoleCommandSender for console access. + * @param debug Enable debug mode. + * @param color Enable colored messages. */ public WRLogger(String prefix, ConsoleCommandSender console, boolean debug, boolean color) { this.debug = debug; this.color = color; - this.console = console; - if (color) this.prefix = ChatColor.RED + prefix + ChatColor.RESET; - else this.prefix = prefix; + this.audience = console != null ? Audience.audience(console) : Audience.empty(); // Use Adventure's Audience + + // Add color to the prefix if applicable + if (color) { + this.prefixComponent = Component.text() + .append(Component.text(prefix, NamedTextColor.RED)) + .append(Component.space()) + .build(); + } else { + this.prefixComponent = Component.text(prefix); + } } /** - * Display a info message to the console. + * Displays an informational message to the console. * - * @param msg Message + * @param msg The informational message. */ public void info(String msg) { - if (color) console.sendMessage(prefix + " " + msg); - else Bukkit.getLogger().info(msg); + log(msg, NamedTextColor.GREEN); } /** - * Display a debug message to the console if debug mode is enabled. + * Displays a debug message to the console if debug mode is enabled. * - * @param msg Message + * @param msg The debug message. */ public void debug(String msg) { if (debug) { - if (color) console.sendMessage(prefix + ChatColor.GOLD + "[Debug] " + ChatColor.RESET + msg); - else Bukkit.getLogger().info(prefix + "[Debug] " + msg); + log("[Debug] " + msg, NamedTextColor.GOLD); } } /** - * Display a severe message to the console. + * Displays a severe error message to the console. * - * @param msg Message + * @param msg The severe message. */ public void severe(String msg) { - if (color) console.sendMessage(prefix + ChatColor.DARK_RED + "[SEVERE] " + ChatColor.RESET + msg); - else Bukkit.getLogger().severe(prefix + " " + msg); + log("[SEVERE] " + msg, NamedTextColor.DARK_RED); } /** - * Display a warning message to the console. + * Displays a warning message to the console. * - * @param msg Message + * @param msg The warning message. */ public void warning(String msg) { - if (color) console.sendMessage(prefix + ChatColor.YELLOW + "[WARNING] " + ChatColor.RESET + msg); - else Bukkit.getLogger().warning(prefix + " " + msg); + log("[WARNING] " + msg, NamedTextColor.YELLOW); + } + + /** + * Logs the message with the appropriate color and level. + * + * @param msg The message to log. + * @param color The text color (or null for no color). + */ + private void log(String msg, TextColor color) { + Component messageComponent = formatMessage(msg, color); + + // Send to audience (Adventure API) + audience.sendMessage(messageComponent); + + // Fallback to Bukkit Logger in case audience isn't available + if (audience == Audience.empty()) { + Bukkit.getLogger().info(stripColors(messageComponent)); + } } -} + /** + * Formats the message by applying prefix and color if needed. + * + * @param msg The message to format. + * @param color The text color (null for no color). + * @return A formatted Component with the message content. + */ + private Component formatMessage(String msg, TextColor color) { + Component msgComponent = Component.text(msg); + if (color != null && this.color) { + msgComponent = msgComponent.color(color); + } + return prefixComponent.append(msgComponent); + } + + /** + * Strips colors from a Component. + * This is a fallback for plain-text logging via Bukkit Logger. + * + * @param component The component to strip. + * @return A plain-text representation of the component. + */ + private String stripColors(Component component) { + return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(component); + } +} \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/WirelessRedstone.java b/core/src/main/java/net/licks92/wirelessredstone/WirelessRedstone.java index 691d5c3f..33ad06d4 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/WirelessRedstone.java +++ b/core/src/main/java/net/licks92/wirelessredstone/WirelessRedstone.java @@ -1,20 +1,17 @@ package net.licks92.wirelessredstone; import io.sentry.Sentry; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import net.licks92.wirelessredstone.commands.Admin.AdminCommandManager; import net.licks92.wirelessredstone.commands.CommandManager; import net.licks92.wirelessredstone.compat.InternalWorldEditHooker; import net.licks92.wirelessredstone.listeners.BlockListener; import net.licks92.wirelessredstone.listeners.PlayerListener; import net.licks92.wirelessredstone.listeners.WorldListener; -import net.licks92.wirelessredstone.sentry.EventExceptionHandler; import net.licks92.wirelessredstone.sentry.WirelessRedstoneSentryClientFactory; -import net.licks92.wirelessredstone.signs.SignType; import net.licks92.wirelessredstone.signs.WirelessReceiver; -import net.licks92.wirelessredstone.signs.WirelessReceiverClock; -import net.licks92.wirelessredstone.signs.WirelessReceiverDelayer; -import net.licks92.wirelessredstone.signs.WirelessReceiverInverter; -import net.licks92.wirelessredstone.signs.WirelessReceiverSwitch; import net.licks92.wirelessredstone.signs.WirelessScreen; import net.licks92.wirelessredstone.signs.WirelessTransmitter; import net.licks92.wirelessredstone.storage.StorageConfiguration; @@ -22,25 +19,24 @@ import net.licks92.wirelessredstone.string.StringManager; import net.licks92.wirelessredstone.string.Strings; import net.licks92.wirelessredstone.worldedit.WorldEditLoader; -import net.licks92.wirelessredstone.materiallib.MaterialLib; import org.bukkit.Bukkit; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.event.Event; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import java.io.InputStreamReader; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Callable; public class WirelessRedstone extends JavaPlugin { public static final String CHANNEL_FOLDER = "channels"; private static WirelessRedstone instance; - private static WRLogger WRLogger; + private static WRLogger wrLogger; // Updated to reflect Adventure Logger private static StringManager stringManager; private static StorageManager storageManager; private static SignManager signManager; @@ -52,32 +48,25 @@ public class WirelessRedstone extends JavaPlugin { private InternalWorldEditHooker worldEditHooker; private boolean storageLoaded = false; private boolean sentryEnabled = true; + private BukkitAudiences audience; // For central Adventure API handling - + // Static getters for easy access public static WirelessRedstone getInstance() { return instance; } public static WRLogger getWRLogger() { - return WRLogger; + return wrLogger; } public static StringManager getStringManager() { return stringManager; } - public static Strings getStrings() { - return getStringManager().getStrings(); - } - public static StorageManager getStorageManager() { return storageManager; } - public static StorageConfiguration getStorage() { - return getStorageManager().getStorage(); - } - public static SignManager getSignManager() { return signManager; } @@ -86,254 +75,220 @@ public static CommandManager getCommandManager() { return commandManager; } - public static AdminCommandManager getAdminCommandManager() { - return adminCommandManager; - } - - public static Metrics getMetrics() { - return metrics; - } - - public boolean isSentryEnabled() { - return sentryEnabled; - } - - public InternalWorldEditHooker getWorldEditHooker() { - return worldEditHooker; - } - - public void setWorldEditHooker(InternalWorldEditHooker worldEditHooker) { - this.worldEditHooker = worldEditHooker; + public static Strings getStrings() { + return stringManager.getStrings(); } @Override public void onEnable() { instance = this; + // Initialize Adventure Audience + audience = BukkitAudiences.create(this); + + // Compatibility check if (!Utils.isCompatible()) { - WRLogger.severe("**********"); - WRLogger.severe("This plugin isn't compatible with this Minecraft version! Please check the bukkit/spigot page."); - WRLogger.severe("**********"); - getPluginLoader().disablePlugin(this); + logIncompatibleVersion(); + return; // Exit early if not compatible } - new MaterialLib(this).initialize(); - + // Initialize logger with Adventure API config = ConfigManager.getConfig(); + wrLogger = new WRLogger("[WirelessRedstone]", (ConsoleCommandSender) audience.console(), config.getDebugMode(), config.getColorLogging()); + + wrLogger.info("Enabling WirelessRedstone"); + + // Load configurations config.update(CHANNEL_FOLDER); + sentryEnabled = config.getSentry() && !"TRUE".equalsIgnoreCase(System.getProperty("mc.development")); - WRLogger = new WRLogger("[WirelessRedstone]", getServer().getConsoleSender(), config.getDebugMode(), config.getColorLogging()); - stringManager = new StringManager(config.getLanguage()); + stringManager = new StringManager(config.getLanguage()); storageManager = new StorageManager(config.getStorageType(), CHANNEL_FOLDER); + // Initialize storage + storageManager = new StorageManager(this); if (!storageManager.getStorage().initStorage()) { - getPluginLoader().disablePlugin(this); - return; + wrLogger.severe("Failed to initialize storage. Plugin will stop."); + + disablePlugin(); // Clean lifecycle management, safely disable + return; // Exit early since we can't continue } storageLoaded = true; + // Initialize managers signManager = new SignManager(); commandManager = new CommandManager(); adminCommandManager = new AdminCommandManager(); - if (sentryEnabled) { - YamlConfiguration pluginConfig = YamlConfiguration.loadConfiguration( - new InputStreamReader(Objects.requireNonNull(getResource("plugin.yml"))) - ); - - getWRLogger().debug("Sentry dsn: " + pluginConfig.getString("sentry.dsn", "")); - - Sentry.init(pluginConfig.getString("sentry.dsn", ""), new WirelessRedstoneSentryClientFactory()); - resetSentryContext(); - } - - PluginManager pm = getServer().getPluginManager(); - - boolean eventCatchingSuccess = true; - try { - if (sentryEnabled) { - EventExceptionHandler eventExceptionHandler = new EventExceptionHandler() { - @Override - public boolean handle(Throwable ex, Event event) { - Sentry.capture(ex); - // getLogger().log(Level.SEVERE, "Error " + ex.getMessage() + " occured for " + event, ex); - - // Don't pass it on - // return true; - // Use Bukkit's default exception handler - return false; - } - }; - - EventExceptionHandler.registerEvents(new WorldListener(), this, eventExceptionHandler); - EventExceptionHandler.registerEvents(new BlockListener(), this, eventExceptionHandler); - EventExceptionHandler.registerEvents(new PlayerListener(), this, eventExceptionHandler); - } - } catch (RuntimeException ex) { - eventCatchingSuccess = false; - getWRLogger().warning("Couldn't register events with Sentry catcher."); - Sentry.capture(ex); - } - - if (!eventCatchingSuccess || !sentryEnabled) { - pm.registerEvents(new WorldListener(), this); - pm.registerEvents(new BlockListener(), this); - pm.registerEvents(new PlayerListener(), this); - } - - getCommand("wirelessredstone").setExecutor(commandManager); - getCommand("wr").setExecutor(commandManager); - getCommand("wredstone").setExecutor(commandManager); - getCommand("wifi").setExecutor(commandManager); + // Initialize Sentry for error reporting + setupSentry(); - getCommand("wirelessredstone").setTabCompleter(commandManager); - getCommand("wr").setTabCompleter(commandManager); - getCommand("wredstone").setTabCompleter(commandManager); - getCommand("wifi").setTabCompleter(commandManager); + // Register events and commands + registerEvents(); + registerCommands(); - getCommand("wradmin").setExecutor(adminCommandManager); - getCommand("wra").setExecutor(adminCommandManager); + // Load WorldEdit integration + loadWorldEditIntegration(); - getCommand("wradmin").setTabCompleter(adminCommandManager); - getCommand("wra").setTabCompleter(adminCommandManager); + // Enable metrics (if applicable) + setupMetrics(); - if (pm.isPluginEnabled("WorldEdit")) { - new WorldEditLoader(); - } + // Check for plugin updates + checkForUpdates(); - if (config.getMetrics()) { - metrics = new Metrics(this); - metrics.addCustomChart(new Metrics.AdvancedPie("main_sign_types", new Callable>() { - @Override - public Map call() { - Map valueMap = new HashMap<>(); - valueMap.put("Transmitters", getSigns(SignType.TRANSMITTER)); - valueMap.put("Receivers", getSigns(SignType.RECEIVER)); - valueMap.put("Screens", getSigns(SignType.SCREEN)); - return valueMap; - } - - private int getSigns(SignType type) { - if (type == SignType.TRANSMITTER) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessTransmitter) - .count(); - } else if (type == SignType.RECEIVER) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiver) - .count(); - } else { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessScreen) - .count(); - } - } - })); - - metrics.addCustomChart(new Metrics.AdvancedPie("receiver_sign_types", new Callable>() { - @Override - public Map call() { - Map valueMap = new HashMap<>(); - valueMap.put("Normal", getSigns(SignType.RECEIVER)); - valueMap.put("Inverter", getSigns(SignType.RECEIVER_INVERTER)); - valueMap.put("Delayer", getSigns(SignType.RECEIVER_DELAYER)); - valueMap.put("Clock", getSigns(SignType.RECEIVER_CLOCK)); - valueMap.put("Switch", getSigns(SignType.RECEIVER_SWITCH)); - return valueMap; - } - - private int getSigns(SignType type) { - if (type == SignType.RECEIVER_INVERTER) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiverInverter) - .count(); - } else if (type == SignType.RECEIVER_DELAYER) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiverDelayer) - .count(); - } else if (type == SignType.RECEIVER_CLOCK) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiverClock) - .count(); - } else if (type == SignType.RECEIVER_SWITCH) { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiverSwitch) - .count(); - } else { - return (int) getStorageManager().getAllSigns().stream() - .filter(point -> point instanceof WirelessReceiver) - .count(); - } - } - })); - - metrics.addCustomChart(new Metrics.SimplePie("storage_types", () -> - ConfigManager.getConfig().getStorageType().toString() - )); - } + wrLogger.info("WirelessRedstone has been successfully enabled!"); + } - if (config.getUpdateCheck()) { - UpdateChecker.init(this).requestUpdateCheck().whenComplete((updateResult, throwable) -> { - if (updateResult.updateAvailable()) { - Bukkit.getScheduler().runTask(this, () -> getWRLogger().info(getStrings().newUpdate - .replaceAll("%%NEWVERSION", updateResult.getNewestVersion()) - .replaceAll("%%URL", updateResult.getUrl()))); - } - }); - } + /** + * Cleanly disables the plugin by throwing an unchecked exception for server shutdown. + * This avoids the use of deprecated `getPluginLoader()`. + */ + private void disablePlugin() { + throw new IllegalStateException("WirelessRedstone was disabled due to a critical error during initialization."); } @Override public void onDisable() { + wrLogger.info("Disabling WirelessRedstone..."); + if (storageLoaded) { - getStorage().close(); + getStorageManager().getStorage().close(); } if (worldEditHooker != null) { worldEditHooker.unRegister(); } - storageLoaded = false; - adminCommandManager = null; - commandManager = null; - signManager = null; - storageManager = null; - stringManager = null; - config = null; - WRLogger = null; + if (audience != null) { + audience.close(); + } + + // Cleanup resources instance = null; + wrLogger = null; + } + + private void logIncompatibleVersion() { + String serverVersion = Bukkit.getBukkitVersion(); + Component errorMsg = Component.text( + "WirelessRedstone is not compatible with server version: " + serverVersion, + NamedTextColor.RED + ); + + // Output the error message using Adventure API + audience.console().sendMessage(errorMsg); + + getPluginLoader().disablePlugin(this); + } + + private void setupMetrics() { + if (!config.getMetrics()) return; + + metrics = new Metrics(this); + + // Main sign type metrics + metrics.addCustomChart(new Metrics.AdvancedPie("main_sign_types", () -> { + try { + return Map.of( + "Transmitters", countSigns(WirelessTransmitter.class), + "Receivers", countSigns(WirelessReceiver.class), + "Screens", countSigns(WirelessScreen.class) + ); + } catch (Exception e) { + wrLogger.warning("Error tracking metrics: " + e.getMessage()); + return Collections.emptyMap(); + } + })); + } + + private void setupSentry() { + if (!sentryEnabled) return; + + try (var resourceStream = getResource("plugin.yml")) { + if (resourceStream == null) { + wrLogger.severe("Could not load 'plugin.yml'. Missing from the jar."); + return; + } + + var pluginConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(resourceStream)); + var sentryDsn = pluginConfig.getString("sentry.dsn", ""); + Sentry.init(sentryDsn, new WirelessRedstoneSentryClientFactory()); + + resetSentryContext(); + + wrLogger.info("Sentry initialized successfully."); + } catch (Exception e) { + wrLogger.severe("Failed to initialize Sentry: " + e.getMessage()); + } } - public void resetSentryContext() { + private void resetSentryContext() { Sentry.clearContext(); + var version = Bukkit.getBukkitVersion().split("-")[0]; + Sentry.getStoredClient().addTag("MC_version", version); + Sentry.getStoredClient().addTag("MC_implementation", "Paper"); + } + + private void registerEvents() { + PluginManager pm = getServer().getPluginManager(); + + pm.registerEvents(new WorldListener(), this); + pm.registerEvents(new BlockListener(), this); + pm.registerEvents(new PlayerListener(), this); - Sentry.getStoredClient().setRelease(getDescription().getVersion()); + wrLogger.info("Events registered successfully."); + } - String version = Bukkit.getBukkitVersion(); - if (version.contains("-")) { - version = version.split("-")[0]; + private void registerCommands() { + Map commands = Map.of( + "wirelessredstone", commandManager, + "wradmin", adminCommandManager + ); + + commands.forEach((commandName, executor) -> { + var command = getCommand(commandName); + if (command != null) { + command.setExecutor(executor); + command.setTabCompleter((TabCompleter) executor); + } else { + wrLogger.warning("Command '" + commandName + "' is missing from plugin.yml."); + } + }); + } + + public static StorageConfiguration getStorage() { + if (storageManager == null || storageManager.getStorage() == null) { + throw new IllegalStateException("StorageManager or StorageConfiguration is not initialized!"); } + return storageManager.getStorage(); + } + - String serverImplementation = "Spigot"; - if (Bukkit.getVersion().contains("Paper")) { - serverImplementation = "Paper"; - } else if (Bukkit.getVersion().contains("Taco")) { - serverImplementation = "TacoSpigot"; + private void loadWorldEditIntegration() { + if (getServer().getPluginManager().isPluginEnabled("WorldEdit")) { + new WorldEditLoader(); + wrLogger.info("WorldEdit integration enabled."); + } else { + wrLogger.warning("WorldEdit not found. Skipping integration."); } + } - Sentry.getStoredClient().addTag("MC_version", version); - Sentry.getStoredClient().addTag("MC_implementation", serverImplementation); + private void checkForUpdates() { + if (!config.getUpdateCheck()) return; + + UpdateChecker.init(this).requestUpdateCheck().whenComplete((result, throwable) -> { + if (throwable == null && result.updateAvailable()) { + wrLogger.info("An update is available: Version " + result.getNewestVersion()); + } + }); } - /** - * Re-initialize strings. This can be used to switch languages after a config change. - *

- * Removes reference to stringManager and place a new reference. - */ - public void resetStrings() { - stringManager = null; - stringManager = new StringManager(config.getLanguage()); + private int countSigns(Class signType) { + if (storageManager == null || storageManager.getAllSigns() == null) return 0; + return (int) storageManager.getAllSigns().stream() + .filter(signType::isInstance) + .count(); } -} +} \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/commands/Admin/AdminAddOwner.java b/core/src/main/java/net/licks92/wirelessredstone/commands/Admin/AdminAddOwner.java index 23b9fe6f..04e3b36e 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/commands/Admin/AdminAddOwner.java +++ b/core/src/main/java/net/licks92/wirelessredstone/commands/Admin/AdminAddOwner.java @@ -8,41 +8,84 @@ import net.licks92.wirelessredstone.WirelessRedstone; import org.bukkit.command.CommandSender; -@CommandInfo(description = "Add owner to WirlessChannel", usage = " ", aliases = {"addowner"}, +@CommandInfo( + description = "Add owner to WirelessChannel", + usage = " ", + aliases = {"addowner"}, tabCompletion = {WirelessCommandTabCompletion.CHANNEL, WirelessCommandTabCompletion.PLAYER}, - permission = "addOwner", canUseInConsole = true, canUseInCommandBlock = false) + permission = "addOwner", + canUseInConsole = true, + canUseInCommandBlock = false +) public class AdminAddOwner extends WirelessCommand { + /** + * Handles the execution of the "addowner" command, which adds a player as an owner + * to a specified wireless channel. The command ensures proper validation of arguments, + * permissions, and checks before adding the owner. + * + * @param sender The entity executing the command. This could be a player, console, or other valid command sender. + * @param args An array of command arguments. The first argument is the channel name, + * and the second is the player's name to be added as an owner. + */ @Override public void onCommand(CommandSender sender, String[] args) { - if (args.length < 2) { - Utils.sendFeedback(WirelessRedstone.getStrings().commandTooFewArguments, sender, true); + // Validate arguments + if (args == null || args.length < 2) { + Utils.sendFeedback(WirelessRedstone.getStrings().commandTooFewArguments, sender, true, null, false); return; } String channelName = args[0]; String playerName = args[1]; + // Validate sender permissions and channel access if (!hasAccessToChannel(sender, channelName)) { - Utils.sendFeedback(WirelessRedstone.getStrings().permissionChannelAccess, sender, true); + Utils.sendFeedback(WirelessRedstone.getStrings().permissionChannelAccess, sender, true, null, false); return; } + // Fetch the channel by name WirelessChannel channel = WirelessRedstone.getStorageManager().getChannel(channelName); if (channel == null) { - Utils.sendFeedback(WirelessRedstone.getStrings().channelNotFound, sender, true); + Utils.sendFeedback(WirelessRedstone.getStrings().channelNotFound(channelName), sender, true, null, false); return; } + // Check if the specified player is already an owner if (channel.getOwners().contains(playerName)) { - Utils.sendFeedback(WirelessRedstone.getStrings().channelAlreadyOwner, sender, true); + Utils.sendFeedback(WirelessRedstone.getStrings().channelAlreadyOwner(playerName), sender, true, null, false); return; } - channel.addOwner(playerName); - WirelessRedstone.getStorage().updateChannel(channelName, channel); + // Attempt to add the owner to the channel + if (addOwnerToChannel(channel, playerName)) { + WirelessRedstone.getStorage().updateChannel(channelName, channel); - WirelessRedstone.getWRLogger().info("Channel " + channelName + " has been updated. Player " + playerName + " has been added to the owner list."); - Utils.sendFeedback(WirelessRedstone.getStrings().channelOwnerAdded.replaceAll("%%PLAYERNAME", playerName), sender, false); + // Notify the sender of successful addition + Utils.sendFeedback( + WirelessRedstone.getStrings().channelOwnerAdded(playerName, channelName), sender, false, null, false); + } else { + // Notify the sender of failure + Utils.sendFeedback("Failed to add the owner. Please try again later.", sender, true, null, false); + } + } + + /** + * Adds an owner to the channel if not already an owner. + * + * @param channel The WirelessChannel object. + * @param playerName The name of the player to add. + * @return True if the player was successfully added, false otherwise. + */ + private boolean addOwnerToChannel(WirelessChannel channel, String playerName) { + try { + channel.addOwner(playerName); // Add the player as an owner + return true; + } catch (IllegalStateException e) { + WirelessRedstone.getWRLogger() + .warning("Failed to add '" + playerName + "' as an owner: " + e.getMessage()); + return false; + } } -} +} \ No newline at end of file diff --git a/core/src/main/java/net/licks92/wirelessredstone/commands/Create.java b/core/src/main/java/net/licks92/wirelessredstone/commands/Create.java index 1e6b03ef..6f682518 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/commands/Create.java +++ b/core/src/main/java/net/licks92/wirelessredstone/commands/Create.java @@ -23,13 +23,13 @@ public void onCommand(CommandSender sender, String[] args) { return; } - if (Utils.getType(args[1]) == null) { + if (Utils.getType(args[1], sign.getLine(2)) == null) { Utils.sendFeedback(WirelessRedstone.getStrings().commandIncorrectSignType, sender, true); return; } String cname = args[0]; - SignType type = Utils.getType(args[1]); + SignType type = Utils.getType(args[1], sign.getLine(2)); if (type == null) { Utils.sendFeedback(WirelessRedstone.getStrings().commandIncorrectSignType, sender, true); diff --git a/core/src/main/java/net/licks92/wirelessredstone/commands/Info.java b/core/src/main/java/net/licks92/wirelessredstone/commands/Info.java index 4a880a75..4b47a657 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/commands/Info.java +++ b/core/src/main/java/net/licks92/wirelessredstone/commands/Info.java @@ -33,7 +33,7 @@ public void onCommand(CommandSender sender, String[] args) { SignType signType = null; if (args.length >= 2) - signType = Utils.getType(args[1]); + signType = Utils.getType(args[1], sign.getLine(2)); if (signType == null) { Utils.sendFeedback(ChatColor.GRAY + "---- " + ChatColor.GREEN + "WirelessChannel " + channel.getName() + ChatColor.GRAY + " ----", diff --git a/core/src/main/java/net/licks92/wirelessredstone/commands/WirelessCommand.java b/core/src/main/java/net/licks92/wirelessredstone/commands/WirelessCommand.java index 357f2ca2..1e788f56 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/commands/WirelessCommand.java +++ b/core/src/main/java/net/licks92/wirelessredstone/commands/WirelessCommand.java @@ -9,7 +9,10 @@ public abstract class WirelessCommand { public abstract void onCommand(CommandSender sender, String[] args); public boolean hasAccessToChannel(CommandSender sender, String channelName) { - return !(sender instanceof Player) || WirelessRedstone.getSignManager().hasAccessToChannel((Player) sender, channelName); //If it's console or commandBlock, it has access to channel. + if (sender instanceof Player) { + WirelessRedstone.getSignManager().hasAccessToChannel((Player) sender, channelName); + } + return true; //If it's console or commandBlock, it has access to channel. } } diff --git a/core/src/main/java/net/licks92/wirelessredstone/listeners/BlockListener.java b/core/src/main/java/net/licks92/wirelessredstone/listeners/BlockListener.java index c68442ae..4cb268cb 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/listeners/BlockListener.java +++ b/core/src/main/java/net/licks92/wirelessredstone/listeners/BlockListener.java @@ -85,7 +85,7 @@ public void on(BlockPhysicsEvent event) { @EventHandler public void on(final SignChangeEvent event) { - SignType signType = Utils.getSignType(event.getLine(0)); + SignType signType = Utils.getType(event.getLine(0), sign.getLine(2)); if (signType == null) { return; } @@ -112,7 +112,7 @@ public void on(final SignChangeEvent event) { return; } - signType = Utils.getSignType(event.getLine(0), event.getLine(2)); + signType = Utils.getType(event.getLine(0), event.getLine(2)); if (event.getLine(1).equalsIgnoreCase("")) { handlePlaceCancelled(event.getBlock()); @@ -156,17 +156,16 @@ public void on(final SignChangeEvent event) { //TODO: #registerSign Implement error message if failed final int finalDelay = delay; Bukkit.getScheduler().runTask(WirelessRedstone.getInstance(), () -> { - if (!(event.getBlock().getState() instanceof Sign)) { + if (!(event.getBlock().getState() instanceof Sign sign)) { return; } - Sign sign = (Sign) event.getBlock().getState(); BlockFace signDirection = InternalProvider.getCompatBlockData().getSignRotation(event.getBlock()); int result = WirelessRedstone.getSignManager().registerSign( channelName, event.getBlock(), - Utils.getSignType(sign.getLine(0), sign.getLine(2)), + Utils.getType(sign.getLine(0), sign.getLine(2)), signDirection, Collections.singletonList(event.getPlayer().getUniqueId().toString()), finalDelay @@ -207,7 +206,7 @@ public void on(BlockBreakEvent event) { if (event.getBlock().getState() instanceof Sign) { Sign sign = (Sign) event.getBlock().getState(); - SignType signType = Utils.getSignType(sign.getLine(0)); + SignType signType = Utils.getType(sign.getLine(0), sign.getLine(2)); if (signType == null) { return; } @@ -335,7 +334,7 @@ private void updateRedstonePower(Sign sign, boolean powered, boolean skipLocatio WirelessRedstone.getWRLogger().debug("Redstone power update (" + powered + "): " + sign.getLocation()); } - if (Utils.getSignType(sign.getLine(0)) != SignType.TRANSMITTER) + if (Utils.getType(sign.getLine(0), sign.getLine(2)) != SignType.TRANSMITTER) return; if (sign.getLine(1).equalsIgnoreCase("")) diff --git a/core/src/main/java/net/licks92/wirelessredstone/listeners/PlayerListener.java b/core/src/main/java/net/licks92/wirelessredstone/listeners/PlayerListener.java index b3b3501a..ffa8f219 100644 --- a/core/src/main/java/net/licks92/wirelessredstone/listeners/PlayerListener.java +++ b/core/src/main/java/net/licks92/wirelessredstone/listeners/PlayerListener.java @@ -40,7 +40,7 @@ public void on(PlayerInteractEvent event) { } Sign sign = (Sign) event.getClickedBlock().getState(); - SignType signType = Utils.getSignType(sign.getLine(0)); + SignType signType = Utils.getType(sign.getLine(0), sign.getLine(2)); if (signType == null) { return; } @@ -67,7 +67,7 @@ public void on(PlayerInteractEvent event) { return; } - signType = Utils.getSignType(sign.getLine(0), sign.getLine(2)); + signType = Utils.getType(sign.getLine(0), sign.getLine(2)); if (sign.getLine(1).equalsIgnoreCase("")) { handlePlaceCancelled(event.getClickedBlock()); @@ -108,7 +108,7 @@ public void on(PlayerInteractEvent event) { int result = WirelessRedstone.getSignManager().registerSign( sign.getLine(1), event.getClickedBlock(), - Utils.getSignType(sign.getLine(0), sign.getLine(2)), + Utils.getType(sign.getLine(0), sign.getLine(2)), signDirection, Collections.singletonList(event.getPlayer().getUniqueId().toString()), delay diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 46498899..37033564 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: WirelessRedstone main: net.licks92.wirelessredstone.WirelessRedstone -api-version: 1.13 +api-version: 1.20.6 website: http://dev.bukkit.org/server-mods/wireless-redstone/ authors: [licks92, Bart_0110] version: ${project.version} diff --git a/dist/pom.xml b/dist/pom.xml index 4e039877..9d6bf306 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -23,7 +23,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.6.0 package diff --git a/pom.xml b/pom.xml index 8b088e50..2fc5de63 100644 --- a/pom.xml +++ b/pom.xml @@ -16,14 +16,14 @@ 4.0.4 UTF-8 - 1.8 - 1.8 + 21 + 21 - 1.14.3 + 1.20.6 ${project.mcversion}-R0.1-SNAPSHOT - 0.9.0 + 0.10.2 - 1.7.23 + 8.3.0 https://7c45ccc4745f428f81d577b15c84b840@sentry.io/1509557 @@ -48,7 +48,7 @@ org.jetbrains annotations - 13.0 + 26.0.2 \ No newline at end of file diff --git a/spigot_1_13/pom.xml b/spigot_1_13/pom.xml index 6e1f379b..dbeea66b 100644 --- a/spigot_1_13/pom.xml +++ b/spigot_1_13/pom.xml @@ -21,10 +21,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.0 - 1.8 - 1.8 + 22 + 22 @@ -34,7 +34,7 @@ org.spigotmc spigot-api - ${project.cbversion} + 1.13.2-R0.1-SNAPSHOT provided diff --git a/spigot_1_8/pom.xml b/spigot_1_8/pom.xml index a124a1d4..186433c7 100644 --- a/spigot_1_8/pom.xml +++ b/spigot_1_8/pom.xml @@ -21,10 +21,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.0 - 1.8 - 1.8 + 22 + 22 diff --git a/worldedit_6/pom.xml b/worldedit_6/pom.xml index 5277a8ed..0de40ebd 100644 --- a/worldedit_6/pom.xml +++ b/worldedit_6/pom.xml @@ -21,10 +21,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.0 - 1.8 - 1.8 + 22 + 22 @@ -32,9 +32,9 @@ - org.spigotmc - spigot-api - 1.12.2-R0.1-SNAPSHOT + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT provided @@ -47,28 +47,33 @@ com.sk89q.worldedit worldedit-core - 6.1 + 7.3.10 provided com.sk89q.worldedit worldedit-bukkit - 6.1 + 7.3.10 provided + + org.yaml + snakeyaml + 2.4 + - spigot-repo - https://hub.spigotmc.org/nexus/content/groups/public/ + papermc + https://repo.papermc.io/repository/maven-public/ - + sk89q-repo - http://maven.sk89q.com/repo/ + https://maven.enginehub.org/repo/ diff --git a/worldedit_7/pom.xml b/worldedit_7/pom.xml index 7c9f903e..a23c9e3b 100644 --- a/worldedit_7/pom.xml +++ b/worldedit_7/pom.xml @@ -21,10 +21,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.0 - 1.8 - 1.8 + 21 + 21 @@ -32,9 +32,9 @@ - org.spigotmc - spigot-api - ${project.cbversion} + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT provided @@ -47,13 +47,13 @@ com.sk89q.worldedit worldedit-core - 7.0.0 + 7.3.10 provided com.sk89q.worldedit worldedit-bukkit - 7.0.0 + 7.3.10 provided @@ -61,14 +61,14 @@ - spigot-repo - https://hub.spigotmc.org/nexus/content/groups/public/ + papermc + https://repo.papermc.io/repository/maven-public/ - sk89q-repo - http://maven.sk89q.com/repo/ + enginehub + https://maven.enginehub.org/repo/ \ No newline at end of file