From 624e49ece43969c6e145fe3653ff7fa60151b2b9 Mon Sep 17 00:00:00 2001 From: Jikoo Date: Sat, 4 Nov 2023 17:43:44 -0400 Subject: [PATCH] Add BukkitVersions to simplify parsing MC/CB versions Bit of a mess, but I think it's about the best I can do. --- .../util/version/BukkitVersions.java | 107 ++++++++++++++++++ .../util/UninstantiableUtilTest.java | 12 ++ .../util/version/BukkitVersionsTest.java | 92 +++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/main/java/com/github/jikoo/planarwrappers/util/version/BukkitVersions.java create mode 100644 src/test/java/com/github/jikoo/planarwrappers/util/version/BukkitVersionsTest.java 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> 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 getMcVersions() { + return List.of( + Arguments.of("1.4.5-R1.0", new IntVersion(1, 4, 5)), + Arguments.of("1.19.4-R0.1-SNAPSHOT", new IntVersion(1, 19, 4)), + Arguments.of("1.20-R0.1-SNAPSHOT", new IntVersion(1, 20, 0)), + Arguments.of("unknown-format+1.10.3-R01 Cool Server Edition", new StringVersion("unknown")), + Arguments.of("1.10.3+coolserver-R0.1-SNAPSHOT", new IntVersion(1, 10, 3)), + Arguments.of("weirdserver+1.10.3-R0.1-SNAPSHOT", new StringVersion("weirdserver")), + Arguments.of("weirdserver-1.10.3-R0.1-SNAPSHOT", new StringVersion("weirdserver")), + Arguments.of("weirdserver+1.10.3", new StringVersion("weirdserver")), + Arguments.of("weirdserver1_10_R3", new StringVersion("weirdserver1_10_R3")) + ); + } + + @ParameterizedTest + @MethodSource("getCbVersions") + void parseCbVersion(String version, Version expected) { + assertThat( + "Version must be parsed from Craftbukkit package", + BukkitVersions.parseCraftbukkitVersion(version), + is(expected)); + } + + private static Collection getCbVersions() { + return List.of( + Arguments.of("org.bukkit.craftbukkit.v1_5_R1.other.package", new IntVersion(1, 5, 1)), + Arguments.of("org.bukkit.craftbukkit.v1_5_R1", new IntVersion(1, 5, 1)), + Arguments.of("org.bukkit.craftbukkit.v18_19_R50", new IntVersion(18, 19, 50)), + Arguments.of("org.bukkit.craftbukkit.v1_20_R2", new IntVersion(1, 20, 2)) + ); + } + +} \ No newline at end of file