diff --git a/src/main/java/com/github/jikoo/planarwrappers/util/version/BukkitVersions.java b/src/main/java/com/github/jikoo/planarwrappers/util/version/BukkitVersions.java new file mode 100644 index 0000000..dbe06be --- /dev/null +++ b/src/main/java/com/github/jikoo/planarwrappers/util/version/BukkitVersions.java @@ -0,0 +1,107 @@ +package com.github.jikoo.planarwrappers.util.version; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; + +public final class BukkitVersions { + + /** + * The Minecraft version. + */ + public static final Version MINECRAFT; + /** + * The {@link Version} of Craftbukkit's package. Do not confuse with {@link #MINECRAFT}! + * + *
The major and minor version correspond to Minecraft's major and minor version. The patch + * version is an internal version that is intended to be bumped whenever the Minecraft mappings + * version ({@code org.bukkit.craftbukkit.util.CraftMagicNumbers#getMappingsVersion}) changes. + * + *
Note that if you are writing a plugin using NMS, you should prefer checking against the
+ * mappings version. Spigot has been very unreliable about bumping the minor version, claiming
+ * that the mappings version is the ultimate way to check if your NMS usage is guaranteed to be
+ * compatible. This is true, but only because they're unreliable about bumping it. If they
+ * actually kept up with bumping it (which could have been scripted) it would be just as reliable
+ * and much more human-friendly.
+ * @deprecated Prefer {@code org.bukkit.craftbukkit.util.CraftMagicNumbers#getMappingsVersion}
+ */
+ @Deprecated
+ public static final Version CRAFTBUKKIT_PACKAGE;
+
+ static {
+ Server server = Bukkit.getServer();
+ // Note: we use Bukkit version and not server version because server version includes prefixes
+ // (such as implementation name and commit hashes of build version) that are not presented in
+ // valid SemVer format.
+ // Bukkit version should be a (mostly) SemVer-compliant version including Minecraft's version.
+ String bukkitVersion = server.getBukkitVersion();
+ MINECRAFT = parseMinecraftVersion(bukkitVersion);
+
+ String packageString = server.getClass().getPackage().toString();
+ CRAFTBUKKIT_PACKAGE = parseCraftbukkitVersion(packageString);
+ }
+
+ @VisibleForTesting
+ @Contract("_ -> new")
+ static @NotNull Version parseMinecraftVersion(@NotNull String bukkitVersion) {
+ Pattern semVerRelease = Pattern.compile("^(\\d+)\\.(\\d+)(\\.(\\d+))?-.*$");
+ Matcher matcher = semVerRelease.matcher(bukkitVersion);
+
+ if (matcher.find()) {
+ String patch = matcher.group(3);
+ return new IntVersion(
+ Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)),
+ patch == null || patch.isEmpty() ? 0 : Integer.parseInt(matcher.group(4))
+ );
+ }
+
+ // If Bukkit version has been modified (oh boy) fall through to using whatever Version selects
+ // with extra SemVer details stripped. Spigot hasn't ever cut a Bukkit release, so
+ // the pre-release data only will serve to cause comparison confusion.
+ return Version.of(stripExtraData(bukkitVersion));
+ }
+
+ @VisibleForTesting
+ static @NotNull Version parseCraftbukkitVersion(@NotNull String craftbukkitPackage) {
+ Pattern packageVer = Pattern.compile("^org\\.bukkit\\.craftbukkit\\.v(\\d+)_(\\d+)_R?(\\d+)");
+ Matcher matcher = packageVer.matcher(craftbukkitPackage);
+
+ if (matcher.find()) {
+ return new IntVersion(
+ Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)),
+ Integer.parseInt(matcher.group(3))
+ );
+ }
+
+ // Ancient/unknown version; package is not versioned. Best bet is to use the Minecraft version.
+ return MINECRAFT;
+ }
+
+ private static @NotNull String stripExtraData(@NotNull String version) {
+ int preReleaseStart = version.indexOf('-');
+ int metadataStart = version.indexOf('+');
+ if (preReleaseStart > -1) {
+ if (metadataStart > -1) {
+ return version.substring(0, Math.min(preReleaseStart, metadataStart));
+ } else {
+ return version.substring(0, preReleaseStart);
+ }
+ } else {
+ if (metadataStart > -1) {
+ return version.substring(0, metadataStart);
+ }
+ }
+ return version;
+ }
+
+ private BukkitVersions() {
+ throw new IllegalStateException("Cannot instantiate static utility classes!");
+ }
+
+}
diff --git a/src/test/java/com/github/jikoo/planarwrappers/util/UninstantiableUtilTest.java b/src/test/java/com/github/jikoo/planarwrappers/util/UninstantiableUtilTest.java
index f03c0e3..e243a82 100644
--- a/src/test/java/com/github/jikoo/planarwrappers/util/UninstantiableUtilTest.java
+++ b/src/test/java/com/github/jikoo/planarwrappers/util/UninstantiableUtilTest.java
@@ -5,7 +5,9 @@
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import com.github.jikoo.planarwrappers.mock.ServerMocks;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -17,7 +19,10 @@
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.params.ParameterizedTest;
@@ -28,6 +33,13 @@ class UninstantiableUtilTest {
private static Collection extends Class>> utilClasses = null;
+ @BeforeAll
+ void setUp() {
+ Server server = ServerMocks.newServer();
+ doReturn("1.0.0-FAKE").when(server).getBukkitVersion();
+ Bukkit.setServer(server);
+ }
+
@ParameterizedTest
@MethodSource("getUtilClasses")
void isFinal(@NotNull Class> clazz) {
diff --git a/src/test/java/com/github/jikoo/planarwrappers/util/version/BukkitVersionsTest.java b/src/test/java/com/github/jikoo/planarwrappers/util/version/BukkitVersionsTest.java
new file mode 100644
index 0000000..a89fa17
--- /dev/null
+++ b/src/test/java/com/github/jikoo/planarwrappers/util/version/BukkitVersionsTest.java
@@ -0,0 +1,92 @@
+package com.github.jikoo.planarwrappers.util.version;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.doReturn;
+
+import com.github.jikoo.planarwrappers.mock.ServerMocks;
+import java.util.Collection;
+import java.util.List;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@TestInstance(Lifecycle.PER_CLASS)
+class BukkitVersionsTest {
+
+ @BeforeAll
+ void beforeAll() {
+ Server server = ServerMocks.newServer();
+ doReturn("1.19.4-R0.1-SNAPSHOT").when(server).getBukkitVersion();
+ Bukkit.setServer(server);
+ }
+
+ @Test
+ void basicParse() {
+ assertThat(BukkitVersions.MINECRAFT, is(new IntVersion(1, 19, 4)));
+ assertThat(BukkitVersions.CRAFTBUKKIT_PACKAGE, is(BukkitVersions.MINECRAFT));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "org.bukkit.craftbukkit.v1_5_RV",
+ "org.bukkit.craftbukkit.1_4_5",
+ "invalid string",
+ "1.4.5-R1.0"
+ })
+ void invalidCbPackage(String packageName) {
+ assertThat(
+ "Invalid package must return Minecraft version",
+ BukkitVersions.parseCraftbukkitVersion(packageName),
+ is(BukkitVersions.MINECRAFT));
+ }
+
+ @ParameterizedTest
+ @MethodSource("getMcVersions")
+ void parseMcVersion(String version, Version expected) {
+ assertThat(
+ "Version must be parsed from Bukkit version",
+ BukkitVersions.parseMinecraftVersion(version),
+ is(expected));
+ }
+
+ private static Collection