diff --git a/README.md b/README.md index c562ea7..418a69d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # BlueBorder Displays the vanilla world border in BlueMap. -Builds are available through [Github Actions](https://github.com/pop4959/BlueBorder/actions). +Builds are available through [GitHub Actions](https://github.com/pop4959/BlueBorder/actions). ## Usage -Simply install BlueBorder into your plugins folder. +Simply install BlueBorder into BlueMap's `packs` folder. -When the plugin is loaded markers will be added to BlueMap for all vanilla world borders. +When the plugin is loaded, markers will be added to BlueMap for all vanilla world borders. -You can force reload the markers with the `/blueborder` command. +You can reload the markers and the config with the `/bluemap reload` command. diff --git a/build.gradle.kts b/build.gradle.kts index 8fc73d7..6e31f13 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,14 @@ plugins { id("java-library") + id("com.gradleup.shadow") version "9.2.2" } group = "org.popcraft" -version = "1.1.2" +version = "2.0.0" java { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(21)) } } @@ -16,19 +17,26 @@ tasks { options.encoding = "UTF-8" } processResources { - filesMatching("plugin.yml") { - expand("version" to project.version) + filesMatching("bluemap.addon.json") { + expand( + "version" to project.version, + "id" to rootProject.name + ) } } + shadowJar { + relocate("com.technicjelle.BMUtils", "org.popcraft.blueborder.shadow.BMUtils") + } } repositories { mavenCentral() - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") - maven("https://jitpack.io") + maven("https://repo.bluecolored.de/releases") } dependencies { - compileOnly("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") - compileOnly("com.github.BlueMap-Minecraft:BlueMapAPI:v2.7.0") + compileOnly("de.bluecolored:bluemap-common:5.13") + compileOnly("de.bluecolored:bluenbt:3.5.0") //bluemap provides it + implementation("com.technicjelle:BMUtils:5.0.3") + implementation("org.spongepowered:configurate-yaml:4.2.0") } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6aba25..aaaabb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..db3a6ac 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/java/org/popcraft/blueborder/BlueBorder.java b/src/main/java/org/popcraft/blueborder/BlueBorder.java index 002c6f4..d4e85b5 100644 --- a/src/main/java/org/popcraft/blueborder/BlueBorder.java +++ b/src/main/java/org/popcraft/blueborder/BlueBorder.java @@ -7,76 +7,126 @@ import de.bluecolored.bluemap.api.markers.ShapeMarker; import de.bluecolored.bluemap.api.math.Color; import de.bluecolored.bluemap.api.math.Shape; -import org.bukkit.World; -import org.bukkit.WorldBorder; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.plugin.java.JavaPlugin; +import de.bluecolored.bluemap.api.plugin.Plugin; +import de.bluecolored.bluemap.common.api.BlueMapWorldImpl; +import de.bluecolored.bluemap.common.api.PluginImpl; +import de.bluecolored.bluemap.core.world.World; +import de.bluecolored.bluemap.core.world.mca.MCAWorld; +import de.bluecolored.bluenbt.BlueNBT; -import java.util.Collections; -import java.util.List; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.GZIPInputStream; -public final class BlueBorder extends JavaPlugin { +public final class BlueBorder implements Runnable { private static final String MARKER_SET_ID = "worldborder"; - private static final String DEFAULT_LABEL = "World border"; - private static final String DEFAULT_COLOR = "FF0000"; - private Color color; - private String label; - @Override - public void onEnable() { - getConfig().options().copyDefaults(true); - saveConfig(); - reloadOptions(); - BlueMapAPI.onEnable(this::addWorldBorders); - BlueMapAPI.onDisable(this::removeWorldBorders); - } + private static final BlueNBT nbt = new BlueNBT(); - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - reloadOptions(); - BlueMapAPI.getInstance().ifPresent(this::removeWorldBorders); - BlueMapAPI.getInstance().ifPresent(this::addWorldBorders); - return true; - } + private Config config; @Override - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - return Collections.emptyList(); + public void run() { + BlueMapAPI.onEnable(this::onEnable); + // No need to remove anything onDisable, because all markers are removed when BlueMap disables, anyway. + // The onEnable simply puts it back. } - private void reloadOptions() { - reloadConfig(); - color = new Color(Integer.parseInt(getConfig().getString("color", DEFAULT_COLOR).toLowerCase(), 16), 1f); - label = getConfig().getString("label", DEFAULT_LABEL); + public void onEnable(BlueMapAPI blueMapAPI) { + // Allow config reloads through `/bluemap reload` + try { + config = Config.load(blueMapAPI); + } catch (IOException e) { + throw new RuntimeException(e); + } + + addWorldBorders(blueMapAPI); } private void addWorldBorders(BlueMapAPI blueMapAPI) { - for (final World world : getServer().getWorlds()) { - final MarkerSet markerSet = MarkerSet.builder().label(label).build(); - final WorldBorder worldBorder = world.getWorldBorder(); - final double centerX = worldBorder.getCenter().getX(); - final double centerZ = worldBorder.getCenter().getZ(); + for (final BlueMapWorld world : blueMapAPI.getWorlds()) { + flushWorldUpdates(blueMapAPI, world); + final MarkerSet markerSet = MarkerSet.builder().label(config.getLabel()).build(); + final WorldBorder worldBorder = getWorldBorder(world); + final double centerX = worldBorder.getX(); + final double centerZ = worldBorder.getZ(); final double radius = worldBorder.getSize() / 2d; final Vector2d pos1 = new Vector2d(centerX - radius, centerZ - radius); final Vector2d pos2 = new Vector2d(centerX + radius, centerZ + radius); final Shape border = Shape.createRect(pos1, pos2); final ShapeMarker marker = ShapeMarker.builder() - .label(label) - .shape(border, world.getSeaLevel()) - .lineColor(color) + .label(config.getLabel()) + .shape(border, config.getHeight()) + .lineColor(config.getColor()) .fillColor(new Color(0)) .lineWidth(3) .depthTestEnabled(false) .build(); - markerSet.getMarkers().put(world.getName(), marker); - blueMapAPI.getWorld(world.getName()) - .map(BlueMapWorld::getMaps) - .ifPresent(maps -> maps.forEach(map -> map.getMarkerSets().put(MARKER_SET_ID, markerSet))); + markerSet.getMarkers().put(world.getId(), marker); + world.getMaps().forEach(map -> map.getMarkerSets().put(MARKER_SET_ID, markerSet)); + } + } + + private static WorldBorder getWorldBorder(BlueMapWorld world) { + final Path worldBorderFile = getWorldBorderFile(world); + + try ( + final InputStream in = Files.newInputStream(worldBorderFile); + final InputStream compressedIn = new BufferedInputStream(new GZIPInputStream(in)) + ) { + return nbt.read(compressedIn, WorldBorder.class); + } catch (IOException e) { + throw new RuntimeException("Failed to read world border data from " + worldBorderFile, e); + } + } + + private static Path getWorldBorderFile(BlueMapWorld world) { + final Path saveFolder = getSaveFolder(world); + final Path worldBorderFile = saveFolder.resolve("data").resolve("minecraft").resolve("world_border.dat"); + if (Files.exists(worldBorderFile)) { + return worldBorderFile; + } + + final Path levelDatFile = saveFolder.resolve("level.dat"); + if (Files.exists(levelDatFile)) { + return levelDatFile; + } + + // on Bukkit, saveFolder() returns paths like `world_nether/DIM-1` for alternate dimensions, so we try the parent as well + final Path levelDatFileBukkitDimension = saveFolder.getParent().resolve("level.dat"); + if (Files.exists(levelDatFileBukkitDimension)) { + return levelDatFileBukkitDimension; } + + throw new RuntimeException("World border file not found for world " + saveFolder); } - private void removeWorldBorders(BlueMapAPI blueMapAPI) { - blueMapAPI.getMaps().forEach(map -> map.getMarkerSets().remove(MARKER_SET_ID)); + // Inspired by https://github.com/BlueMap-Minecraft/BlueMap/blob/3092de2e2320fef2081ddb5e5f1040846a3103f9/common/src/main/java/de/bluecolored/bluemap/common/api/BlueMapWorldImpl.java#L60-L69 + // But that method is deprecated, so we reimplement it here + private static Path getSaveFolder(BlueMapWorld apiWorld) { + BlueMapWorldImpl worldImpl = (BlueMapWorldImpl) apiWorld; + World world = worldImpl.world(); + if (world instanceof MCAWorld mcaWorld) { + return mcaWorld.getDimensionFolder(); + } else { + throw new UnsupportedOperationException("Unsupported world type: " + world.getClass().getName()); + } + } + + /// Ensure that the world_border.dat or level.dat file is flushed to disk, so it can be read. + /// This is important in case a user just changed the world border and reloaded BlueMap to see the changes on their map. + private static void flushWorldUpdates(BlueMapAPI api, BlueMapWorld apiWorld) { + Plugin plugin = api.getPlugin(); + PluginImpl pluginImpl = (PluginImpl) plugin; + BlueMapWorldImpl worldImpl = (BlueMapWorldImpl) apiWorld; + World world = worldImpl.world(); + try { + pluginImpl.getPlugin().flushWorldUpdates(world); + } catch (IOException e) { + throw new RuntimeException("Failed to flush world updates for " + world.getName(), e); + } } } diff --git a/src/main/java/org/popcraft/blueborder/Config.java b/src/main/java/org/popcraft/blueborder/Config.java new file mode 100644 index 0000000..1df22b8 --- /dev/null +++ b/src/main/java/org/popcraft/blueborder/Config.java @@ -0,0 +1,52 @@ +package org.popcraft.blueborder; + +import com.technicjelle.BMUtils.BMNative.BMNConfigDirectory; +import de.bluecolored.bluemap.api.BlueMapAPI; +import de.bluecolored.bluemap.api.math.Color; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.IOException; +import java.nio.file.Path; + +@ConfigSerializable +public class Config { + private static final String fileName = "config.yml"; + + private static final String DEFAULT_LABEL = "World border"; + private static final String DEFAULT_COLOR = "FF0000"; + private static final int DEFAULT_HEIGHT = 63; + + private @Nullable String color; + private @Nullable String label; + private @Nullable Integer height; + + public static Config load(BlueMapAPI api) throws IOException { + BMNConfigDirectory.BMNCopy.fromJarResource(api, Config.class.getClassLoader(), fileName, fileName, false); + Path configDirectory = BMNConfigDirectory.getAllocatedDirectory(api, Config.class.getClassLoader()); + Path configFile = configDirectory.resolve(fileName); + + YamlConfigurationLoader loader = YamlConfigurationLoader.builder() + .defaultOptions(options -> options.implicitInitialization(false)) + .path(configFile).build(); + + Config config = loader.load().get(Config.class); + if (config == null) { + throw new IOException("Failed to load config"); + } + return config; + } + + public Color getColor() { + return new Color(Integer.parseInt((color != null ? color : DEFAULT_COLOR).toLowerCase(), 16), 1f); + } + + public String getLabel() { + return label != null ? label : DEFAULT_LABEL; + } + + public int getHeight() { + return height != null ? height : DEFAULT_HEIGHT; + } +} diff --git a/src/main/java/org/popcraft/blueborder/WorldBorder.java b/src/main/java/org/popcraft/blueborder/WorldBorder.java new file mode 100644 index 0000000..ef780c4 --- /dev/null +++ b/src/main/java/org/popcraft/blueborder/WorldBorder.java @@ -0,0 +1,31 @@ +package org.popcraft.blueborder; + +import de.bluecolored.bluenbt.NBTName; + +public class WorldBorder { + @NBTName({"data", "Data"}) + private WorldBorderData data; + + public double getX() { + return data.centerX; + } + + public double getZ() { + return data.centerZ; + } + + public double getSize() { + return data.size; + } + + private static class WorldBorderData { + @NBTName({"center_x", "BorderCenterX"}) + private double centerX; + + @NBTName({"center_z", "BorderCenterZ"}) + private double centerZ; + + @NBTName({"size", "BorderSize"}) + private double size; + } +} diff --git a/src/main/resources/bluemap.addon.json b/src/main/resources/bluemap.addon.json new file mode 100644 index 0000000..a0801c1 --- /dev/null +++ b/src/main/resources/bluemap.addon.json @@ -0,0 +1,5 @@ +{ + "id": "${id}", + "entrypoint": "org.popcraft.blueborder.BlueBorder", + "version": "${version}" +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 15fad89..8d7ed99 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,2 +1,3 @@ label: World border color: FF0000 +height: 63 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml deleted file mode 100644 index eb1401e..0000000 --- a/src/main/resources/plugin.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: BlueBorder -version: ${version} -main: org.popcraft.blueborder.BlueBorder -api-version: 1.17 -depend: [ BlueMap ] -authors: [ pop4959 ] -description: Displays the vanilla world border in BlueMap. -commands: - blueborder: - permission: blueborder.command - description: Updates vanilla world border markers. -permissions: - blueborder.command: - default: op