diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java index b1c5ac909..7700445cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java @@ -31,7 +31,7 @@ import java.util.regex.Pattern; /** * @author Glavo */ -public abstract class GameVersionNumber implements Comparable { +public abstract sealed class GameVersionNumber implements Comparable { public static String[] getDefaultGameVersions() { return Versions.DEFAULT_GAME_VERSIONS; @@ -103,8 +103,7 @@ public abstract class GameVersionNumber implements Comparable if (this instanceof Special) return true; - if (this instanceof Snapshot) { - Snapshot snapshot = (Snapshot) this; + if (this instanceof Snapshot snapshot) { return snapshot.intValue == Snapshot.toInt(15, 14, 'a'); } @@ -131,6 +130,39 @@ public abstract class GameVersionNumber implements Comparable return compareToImpl(other); } + /// @see #isAtLeast(String, String, boolean) + public boolean isAtLeast(@NotNull String releaseVersion, @NotNull String snapshotVersion) { + return isAtLeast(releaseVersion, snapshotVersion, false); + } + + /// When comparing between Release Version and Snapshot Version, it is necessary to load `/assets/game/versions.txt` and perform a lookup, which is less efficient. + /// Therefore, when checking whether a version contains a certain feature, you should use this method and provide both the first release version and the exact snapshot version that introduced the feature, + /// so that the comparison can be performed quickly without a lookup. + /// + /// For example, the datapack feature was introduced in Minecraft 1.13, and more specifically in snapshot `17w43a`. + /// So you can test whether a game version supports datapacks like this: + /// + /// ```java + /// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a"); + /// ``` + /// + /// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than + /// its corresponding pre/rc versions. + public boolean isAtLeast(@NotNull String releaseVersion, @NotNull String snapshotVersion, boolean strictReleaseVersion) { + if (this instanceof Release self) { + Release other; + if (strictReleaseVersion) { + other = Release.parse(releaseVersion); + } else { + other = Release.parseSimple(releaseVersion); + } + + return self.compareToRelease(other) >= 0; + } else { + return this.compareTo(Snapshot.parse(snapshotVersion)) >= 0; + } + } + @Override public String toString() { return value; @@ -201,9 +233,7 @@ public abstract class GameVersionNumber implements Comparable @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof Old)) return false; - Old other = (Old) o; - return type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0; + return o instanceof Old other && type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0; } @Override @@ -256,6 +286,52 @@ public abstract class GameVersionNumber implements Comparable return new Release(value, 1, minor, patch, eaType, eaVersion); } + private static int getNumberLength(String value, int offset) { + int current = offset; + while (current < value.length()) { + char ch = value.charAt(current); + if (ch < '0' || ch > '9') + break; + + current++; + } + + return current - offset; + } + + /// Quickly parses a simple format (`1\.[0-9]+(\.[0-9]+)?`) release version. + /// The returned [#eaType] will be set to [#TYPE_UNKNOWN], meaning it will be less than all pre/rc and official versions of this version. + /// + /// @see GameVersionNumber#isAtLeast(String, String) + static Release parseSimple(String value) { + if (!value.startsWith("1.")) + throw new IllegalArgumentException(value); + + final int minorOffset = 2; + + int minorLength = getNumberLength(value, minorOffset); + if (minorLength == 0) + throw new IllegalArgumentException(value); + + try { + int minor = Integer.parseInt(value.substring(minorOffset, minorOffset + minorLength)); + int patch = 0; + + if (minorOffset + minorLength < value.length()) { + int patchOffset = minorOffset + minorLength + 1; + + if (patchOffset >= value.length() || value.charAt(patchOffset - 1) != '.') + throw new IllegalArgumentException(value); + + patch = Integer.parseInt(value.substring(patchOffset)); + } + + return new Release(value, 1, minor, patch, TYPE_UNKNOWN, VersionNumber.ZERO); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(value); + } + } + private final int major; private final int minor; private final int patch; @@ -280,24 +356,20 @@ public abstract class GameVersionNumber implements Comparable int compareToRelease(Release other) { int c = Integer.compare(this.major, other.major); - if (c != 0) { + if (c != 0) return c; - } c = Integer.compare(this.minor, other.minor); - if (c != 0) { + if (c != 0) return c; - } c = Integer.compare(this.patch, other.patch); - if (c != 0) { + if (c != 0) return c; - } c = Integer.compare(this.eaType, other.eaType); - if (c != 0) { + if (c != 0) return c; - } return this.eaVersion.compareTo(other.eaVersion); } @@ -356,9 +428,12 @@ public abstract class GameVersionNumber implements Comparable @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Release other = (Release) o; - return major == other.major && minor == other.minor && patch == other.patch && eaType == other.eaType && eaVersion.equals(other.eaVersion); + return o instanceof Release other + && major == other.major + && minor == other.minor + && patch == other.patch + && eaType == other.eaType + && eaVersion.equals(other.eaVersion); } } @@ -428,9 +503,7 @@ public abstract class GameVersionNumber implements Comparable @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Snapshot other = (Snapshot) o; - return this.intValue == other.intValue; + return o instanceof Snapshot other && this.intValue == other.intValue; } @Override @@ -534,9 +607,7 @@ public abstract class GameVersionNumber implements Comparable @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Special other = (Special) o; - return Objects.equals(this.value, other.value); + return o instanceof Special other && this.value.equals(other.value); } } @@ -566,8 +637,7 @@ public abstract class GameVersionNumber implements Comparable if (currentRelease == null) currentRelease = (Release) version; - if (version instanceof Snapshot) { - Snapshot snapshot = (Snapshot) version; + if (version instanceof Snapshot snapshot) { snapshots.add(snapshot); snapshotPrev.add(currentRelease); } else if (version instanceof Release) { @@ -576,8 +646,7 @@ public abstract class GameVersionNumber implements Comparable if (currentRelease.eaType == Release.TYPE_GA) { defaultGameVersions.addFirst(currentRelease.value); } - } else if (version instanceof Special) { - Special special = (Special) version; + } else if (version instanceof Special special) { special.prev = prev; SPECIALS.put(special.value, special); } else diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/GameVersionNumberTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/GameVersionNumberTest.java index 1b1e579c6..23fa1fc7e 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/GameVersionNumberTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/GameVersionNumberTest.java @@ -26,6 +26,7 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.*; +import static org.jackhuang.hmcl.util.versioning.GameVersionNumber.asGameVersion; import static org.junit.jupiter.api.Assertions.*; /** @@ -66,8 +67,8 @@ public final class GameVersionNumberTest { } private static void assertGameVersionEquals(String version1, String version2) { - assertEquals(0, GameVersionNumber.asGameVersion(version1).compareTo(version2), errorMessage(version1, version2)); - assertEquals(GameVersionNumber.asGameVersion(version1), GameVersionNumber.asGameVersion(version2), errorMessage(version1, version2)); + assertEquals(0, asGameVersion(version1).compareTo(version2), errorMessage(version1, version2)); + assertEquals(asGameVersion(version1), asGameVersion(version2), errorMessage(version1, version2)); } private static String toString(GameVersionNumber gameVersionNumber) { @@ -76,12 +77,12 @@ public final class GameVersionNumberTest { private static void assertOrder(String... versions) { for (int i = 0; i < versions.length - 1; i++) { - GameVersionNumber version1 = GameVersionNumber.asGameVersion(versions[i]); + GameVersionNumber version1 = asGameVersion(versions[i]); assertGameVersionEquals(versions[i]); for (int j = i + 1; j < versions.length; j++) { - GameVersionNumber version2 = GameVersionNumber.asGameVersion(versions[j]); + GameVersionNumber version2 = asGameVersion(versions[j]); assertEquals(-1, version1.compareTo(version2), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2))); assertEquals(1, version2.compareTo(version1), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2))); @@ -92,7 +93,7 @@ public final class GameVersionNumberTest { } private void assertOldVersion(String oldVersion, GameVersionNumber.Type type, String versionNumber) { - GameVersionNumber version = GameVersionNumber.asGameVersion(oldVersion); + GameVersionNumber version = asGameVersion(oldVersion); assertInstanceOf(GameVersionNumber.Old.class, version); GameVersionNumber.Old old = (GameVersionNumber.Old) version; assertSame(type, old.type); @@ -100,7 +101,7 @@ public final class GameVersionNumberTest { } private static boolean isAprilFools(String version) { - return GameVersionNumber.asGameVersion(version).isAprilFools(); + return asGameVersion(version).isAprilFools(); } @Test @@ -142,10 +143,36 @@ public final class GameVersionNumberTest { public void testParseNew() { List versions = readVersions(); for (String version : versions) { - assertFalse(GameVersionNumber.asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version); + assertFalse(asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version); } } + private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int minor, int patch) { + GameVersionNumber.Release release = GameVersionNumber.Release.parseSimple(simpleReleaseVersion); + assertAll("Assert Simple Release Version " + simpleReleaseVersion, + () -> assertEquals(1, release.getMajor()), + () -> assertEquals(minor, release.getMinor()), + () -> assertEquals(patch, release.getPatch()), + () -> assertEquals(GameVersionNumber.Release.TYPE_UNKNOWN, release.getEaType()), + () -> assertEquals(VersionNumber.ZERO, release.getEaVersion()) + ); + } + + @Test + public void testParseSimpleRelease() { + assertSimpleReleaseVersion("1.0", 0, 0); + assertSimpleReleaseVersion("1.13", 13, 0); + assertSimpleReleaseVersion("1.21.8", 21, 8); + + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("2.0")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1..0")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0.")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.a")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.1a")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0a")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0.0.0")); + } + @Test public void testCompareRelease() { assertGameVersionEquals("0.0"); @@ -281,4 +308,27 @@ public final class GameVersionNumberTest { "1.1" ); } + + @Test + public void isAtLeast() { + assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a")); + assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a")); + + assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a")); + assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a")); + assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a")); + assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a")); + assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a")); + assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a")); + + assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a")); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13")); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime")); + } }