diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index a859aa284..bec53d561 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -338,7 +338,7 @@ public final class HMCLGameRepository extends DefaultGameRepository { GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion); if (versionNumber.isAprilFools()) { return VersionIconType.APRIL_FOOLS.getIcon(); - } else if (versionNumber instanceof GameVersionNumber.Snapshot) { + } else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) { return VersionIconType.COMMAND.getIcon(); } else if (versionNumber instanceof GameVersionNumber.Old) { return VersionIconType.CRAFT_TABLE.getIcon(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 837e3edfb..7ffc699a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -233,7 +233,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab } setGraphic(pane); - twoLineListItem.setTitle(I18n.getDisplaySelfVersion(remoteVersion)); + twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion)); if (remoteVersion.getReleaseDate() != null) { twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate())); } else { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java index b1d910b69..00088d5bc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java @@ -39,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.i18n.I18n; import org.jetbrains.annotations.NotNull; import java.nio.file.*; @@ -245,7 +246,7 @@ public final class WorldBackupsPage extends ListPageBase { item.setMouseTransparent(true); if (world.getWorldName() != null) item.setTitle(parseColorEscapes(world.getWorldName())); - item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion())); + item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())))); if (world.getGameVersion() != null) - item.addTag(world.getGameVersion()); + item.addTag(I18n.getDisplayVersion(world.getGameVersion())); if (world.isLocked()) item.addTag(i18n("world.locked")); } @@ -126,7 +126,7 @@ public final class WorldListItemSkin extends SkinBase { new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup) ); - if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) { + if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) { popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index e39162f31..7715bfc15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; @@ -49,7 +50,7 @@ public final class WorldListPage extends ListPageBase implements private List worlds; private Profile profile; private String id; - private String gameVersion; + private GameVersionNumber gameVersion; public WorldListPage() { FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { @@ -87,7 +88,7 @@ public final class WorldListPage extends ListPageBase implements return; setLoading(true); - Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).orElse(null)) + Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null)) .thenApplyAsync(unused -> { try (Stream stream = World.getWorlds(savesDir)) { return stream.parallel().collect(Collectors.toList()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 64679346b..04c2fc74b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -33,7 +33,6 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.ChunkBaseApp; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; import java.nio.channels.FileChannel; @@ -84,7 +83,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco .addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL); if (world.getGameVersion() != null && // old game will not write game version to level.dat - GameVersionNumber.asGameVersion(world.getGameVersion()).isAtLeast("1.13", "17w43a")) { + world.getGameVersion().isAtLeast("1.13", "17w43a")) { header.getTabs().add(datapackTab); sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL); } @@ -102,7 +101,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup) ); - if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) { + if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) { popupMenu.getContent().add( new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java index f5aff2293..8556161bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java @@ -49,7 +49,7 @@ public final class ChunkBaseApp { public static boolean isSupported(@NotNull World world) { return world.getSeed() != null && world.getGameVersion() != null && - GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo(MIN_GAME_VERSION) >= 0; + world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0; } public static ChunkBaseApp newBuilder(String app, long seed) { @@ -60,7 +60,7 @@ public final class ChunkBaseApp { assert isSupported(world); newBuilder("seed-map", Objects.requireNonNull(world.getSeed())) - .addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS) + .addPlatform(world.getGameVersion(), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS) .open(); } @@ -68,7 +68,7 @@ public final class ChunkBaseApp { assert isSupported(world); newBuilder("stronghold-finder", Objects.requireNonNull(world.getSeed())) - .addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS) + .addPlatform(world.getGameVersion(), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS) .open(); } @@ -76,7 +76,7 @@ public final class ChunkBaseApp { assert isSupported(world); newBuilder("nether-fortress-finder", Objects.requireNonNull(world.getSeed())) - .addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, NETHER_FORTRESS_GAME_VERSIONS) + .addPlatform(world.getGameVersion(), false, NETHER_FORTRESS_GAME_VERSIONS) .open(); } @@ -84,7 +84,7 @@ public final class ChunkBaseApp { assert isSupported(world); newBuilder("endcity-finder", Objects.requireNonNull(world.getSeed())) - .addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, END_CITY_GAME_VERSIONS) + .addPlatform(world.getGameVersion(), false, END_CITY_GAME_VERSIONS) .open(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java index 15bff4be8..286910be0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.util.i18n; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.util.i18n.translator.Translator; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.PropertyKey; @@ -76,7 +77,11 @@ public final class I18n { return getTranslator().formatSpeed(bytes); } - public static String getDisplaySelfVersion(RemoteVersion version) { + public static String getDisplayVersion(RemoteVersion version) { + return getTranslator().getDisplayVersion(version); + } + + public static String getDisplayVersion(GameVersionNumber version) { return getTranslator().getDisplayVersion(version); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java index 4a5edc2ad..382124fe9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java @@ -72,7 +72,7 @@ public final class MinecraftWiki { else if (wikiVersion.startsWith("1.0.0-rc2")) wikiVersion = "1.0.0-rc2"; } - } else if (gameVersion instanceof GameVersionNumber.Snapshot) { + } else if (gameVersion instanceof GameVersionNumber.LegacySnapshot) { return locale.i18n("wiki.version.game.snapshot", wikiVersion) + variantSuffix; } else { if (wikiVersion.length() >= 6 && wikiVersion.charAt(2) == 'w') { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java index 477871fc8..7bdcfc803 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.i18n.translator; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.util.i18n.SupportedLocale; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -47,6 +48,10 @@ public class Translator { return remoteVersion.getSelfVersion(); } + public String getDisplayVersion(GameVersionNumber versionNumber) { + return versionNumber.toNormalizedString(); + } + /// @see [#formatDateTime(TemporalAccessor)] protected DateTimeFormatter dateTimeFormatter; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java index 3bf7b6474..a6302a533 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java @@ -92,22 +92,38 @@ public class Translator_lzh extends Translator { appendDigitByDigit(builder, String.valueOf(release.getPatch())); } - //noinspection StatementWithEmptyBody - if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) { - // do nothing - } else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) { - builder.append("之預"); - appendDigitByDigit(builder, release.getEaVersion().toString()); - } else if (release.getEaType() == GameVersionNumber.Release.TYPE_RC) { - builder.append("之候"); - appendDigitByDigit(builder, release.getEaVersion().toString()); - } else { - // Unsupported - return gameVersion.toString(); + switch (release.getEaType()) { + case GA -> { + // do nothing + } + case PRE_RELEASE -> { + builder.append("之預"); + appendDigitByDigit(builder, release.getEaVersion().toString()); + } + case RELEASE_CANDIDATE -> { + builder.append("之候"); + appendDigitByDigit(builder, release.getEaVersion().toString()); + } + default -> { + // Unsupported + return gameVersion.toString(); + } + } + + switch (release.getAdditional()) { + case NONE -> { + } + case UNOBFUSCATED -> { + builder.append("涇渭"); + } + default -> { + // Unsupported + return gameVersion.toString(); + } } return builder.toString(); - } else if (gameVersion instanceof GameVersionNumber.Snapshot snapshot) { + } else if (gameVersion instanceof GameVersionNumber.LegacySnapshot snapshot) { StringBuilder builder = new StringBuilder(); appendDigitByDigit(builder, String.valueOf(snapshot.getYear())); @@ -120,6 +136,9 @@ public class Translator_lzh extends Translator { else builder.append(suffix); + if (snapshot.isUnobfuscated()) + builder.append("涇渭"); + return builder.toString(); } else if (gameVersion instanceof GameVersionNumber.Special) { String version = gameVersion.toString(); @@ -130,6 +149,7 @@ public class Translator_lzh extends Translator { case "2.0_purple" -> "二點〇紫"; case "1.rv-pre1" -> "一點真視之預一"; case "3d shareware v1.34" -> "躍然享件一點三四"; + case "13w12~" -> "一三週一二閏"; case "20w14infinite", "20w14~", "20w14∞" -> "二〇週一四宇"; case "22w13oneblockatatime" -> "二二週一三典"; case "23w13a_or_b" -> "二三週一三暨"; @@ -179,6 +199,11 @@ public class Translator_lzh extends Translator { return translateGenericVersion(remoteVersion.getSelfVersion()); } + @Override + public String getDisplayVersion(GameVersionNumber versionNumber) { + return translateGameVersion(versionNumber); + } + @Override public String formatDateTime(TemporalAccessor time) { LocalDateTime localDateTime; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 8d21dc25d..7273d2d20 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -24,6 +24,7 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import javafx.scene.image.Image; import org.jackhuang.hmcl.util.io.*; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -48,7 +49,7 @@ public final class World { private final Path file; private String fileName; private String worldName; - private String gameVersion; + private GameVersionNumber gameVersion; private long lastPlayed; private Image icon; private Long seed; @@ -108,7 +109,7 @@ public final class World { return lastPlayed; } - public String getGameVersion() { + public @Nullable GameVersionNumber getGameVersion() { return gameVersion; } @@ -188,7 +189,7 @@ public final class World { CompoundTag version = data.get("Version"); if (version.get("Name") instanceof StringTag) - gameVersion = version.get("Name").getValue(); + gameVersion = GameVersionNumber.asGameVersion(version.get("Name").getValue()); } Tag worldGenSettings = data.get("WorldGenSettings"); 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 40f4f8193..5f7863817 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 @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util.versioning; -import org.intellij.lang.annotations.MagicConstant; +import org.jackhuang.hmcl.util.ToStringBuilder; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; @@ -28,6 +28,8 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + /** * @author Glavo */ @@ -38,6 +40,10 @@ public abstract sealed class GameVersionNumber implements Comparable= 6 && version.charAt(2) == 'w') + return LegacySnapshot.parse(version); - if (version.length() == 6 && version.charAt(2) == 'w') - return Snapshot.parse(version); + return Release.parse(version); } - } catch (IllegalArgumentException ignore) { + } catch (Throwable ignore) { } - Special special = Versions.SPECIALS.get(version); - if (special == null) { - special = new Special(version); - } - return special; + return new Special(version, version); } public static GameVersionNumber asGameVersion(Optional version) { @@ -94,17 +95,22 @@ public abstract sealed class GameVersionNumber implements Comparable= 0; } else { - return this.compareTo(Snapshot.parse(snapshotVersion)) >= 0; + return this.compareTo(LegacySnapshot.parse(snapshotVersion)) >= 0; } } + public String toNormalizedString() { + return normalized; + } + @Override public String toString() { return value; } + protected ToStringBuilder buildDebugString() { + return new ToStringBuilder(this) + .append("value", value) + .append("normalized", normalized) + .append("type", getType()); + } + + public final String toDebugString() { + return buildDebugString().toString(); + } + public static final class Old extends GameVersionNumber { static Old parse(String value) { + if (value.isEmpty()) + throw new IllegalArgumentException("Empty old version number"); + Type type; int prefixLength = 1; switch (value.charAt(0)) { @@ -215,7 +239,7 @@ public abstract sealed class GameVersionNumber implements Comparable[0-9]+)(\\.(?[0-9]+))?((?(-[a-zA-Z]+| Pre-Release ))(?.+))?"); + public enum ReleaseType { + UNKNOWN(""), + SNAPSHOT("-snapshot-"), + PRE_RELEASE("-pre"), + RELEASE_CANDIDATE("-rc"), + GA(""); + private final String infix; - public static final int TYPE_GA = Integer.MAX_VALUE; + ReleaseType(String infix) { + this.infix = infix; + } + } - public static final int TYPE_UNKNOWN = 0; - public static final int TYPE_EXP = 1; - public static final int TYPE_PRE = 2; - public static final int TYPE_RC = 3; + public enum Additional { + NONE(""), UNOBFUSCATED("_unobfuscated"); + private final String suffix; - static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO); + Additional(String suffix) { + this.suffix = suffix; + } + } + + static final Release ZERO = new Release( + "0.0", "0.0", + 0, 0, 0, + ReleaseType.UNKNOWN, VersionNumber.ZERO, Additional.NONE + ); + + private static final Pattern VERSION_PATTERN = Pattern.compile("(?(?1|[1-9]\\d+)\\.(?\\d+)(\\.(?[0-9]+))?)(?.*)"); static Release parse(String value) { - Matcher matcher = PATTERN.matcher(value); + Matcher matcher = VERSION_PATTERN.matcher(value); if (!matcher.matches()) { throw new IllegalArgumentException(value); } + int major = Integer.parseInt(matcher.group("major")); + if (major != 1 && major < MINIMUM_YEAR_MAJOR_VERSION) + throw new IllegalArgumentException(value); + int minor = Integer.parseInt(matcher.group("minor")); String patchString = matcher.group("patch"); int patch = patchString != null ? Integer.parseInt(patchString) : 0; - String eaTypeString = matcher.group("eaType"); - int eaType; - if (eaTypeString == null) { - eaType = TYPE_GA; - } else if ("-pre".equals(eaTypeString) || " Pre-Release ".equals(eaTypeString)) { - eaType = TYPE_PRE; - } else if ("-rc".equals(eaTypeString)) { - eaType = TYPE_RC; - } else if ("-exp".equals(eaTypeString)) { - eaType = TYPE_EXP; + String suffix = matcher.group("suffix"); + + ReleaseType releaseType; + VersionNumber eaVersion; + Additional additional = Additional.NONE; + boolean needNormalize = false; + + if (suffix.endsWith("_unobfuscated")) { + suffix = suffix.substring(0, suffix.length() - "_unobfuscated".length()); + additional = Additional.UNOBFUSCATED; + } else if (suffix.endsWith(" Unobfuscated")) { + needNormalize = true; + suffix = suffix.substring(0, suffix.length() - " Unobfuscated".length()); + additional = Additional.UNOBFUSCATED; + } + + if (suffix.isEmpty()) { + releaseType = ReleaseType.GA; + eaVersion = VersionNumber.ZERO; + } else if (suffix.startsWith("-snapshot-")) { + releaseType = ReleaseType.SNAPSHOT; + eaVersion = VersionNumber.asVersion(suffix.substring("-snapshot-".length())); + } else if (suffix.startsWith("-pre")) { + releaseType = ReleaseType.PRE_RELEASE; + eaVersion = VersionNumber.asVersion(suffix.substring("-pre".length())); + } else if (suffix.startsWith(" Pre-Release ")) { + needNormalize = true; + releaseType = ReleaseType.PRE_RELEASE; + eaVersion = VersionNumber.asVersion(suffix.substring(" Pre-Release ".length())); + } else if (suffix.startsWith("-rc")) { + releaseType = ReleaseType.RELEASE_CANDIDATE; + eaVersion = VersionNumber.asVersion(suffix.substring("-rc".length())); + } else if (suffix.startsWith(" Release Candidate ")) { + needNormalize = true; + releaseType = ReleaseType.RELEASE_CANDIDATE; + eaVersion = VersionNumber.asVersion(suffix.substring(" Release Candidate ".length())); } else { - eaType = TYPE_UNKNOWN; + throw new IllegalArgumentException(value); } - String eaVersionString = matcher.group("eaVersion"); - VersionNumber eaVersion = eaVersionString != null ? VersionNumber.asVersion(eaVersionString) : VersionNumber.ZERO; - - 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++; + String normalized; + if (needNormalize) { + StringBuilder builder = new StringBuilder(value.length()); + builder.append(matcher.group("prefix")); + if (releaseType != ReleaseType.GA) { + builder.append(releaseType.infix); + builder.append(eaVersion); + } + builder.append(additional.suffix); + normalized = builder.toString(); + } else { + normalized = value; } - return current - offset; + return new Release(value, normalized, major, minor, patch, releaseType, eaVersion, additional); } - /// 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. + /// Quickly parses a simple format (`[1-9][0-9]+\.[0-9]+(\.[0-9]+)?`) release version. + /// The returned [#eaType] will be set to [ReleaseType#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.")) + int majorLength = getNumberLength(value, 0); + if (majorLength == 0 || value.length() < majorLength + 2 || value.charAt(majorLength) != '.') throw new IllegalArgumentException(value); - final int minorOffset = 2; + int major = Integer.parseInt(value.substring(0, majorLength)); + if (major != 1 && major < MINIMUM_YEAR_MAJOR_VERSION) + throw new IllegalArgumentException(value); + + final int minorOffset = majorLength + 1; int minorLength = getNumberLength(value, minorOffset); if (minorLength == 0) @@ -326,27 +404,41 @@ public abstract sealed class GameVersionNumber implements Comparable '9') + break; + + current++; + } + + return current - offset; + } + private final int major; private final int minor; private final int patch; - @MagicConstant(intValues = {TYPE_GA, TYPE_UNKNOWN, TYPE_EXP, TYPE_PRE, TYPE_RC}) - private final int eaType; + private final ReleaseType eaType; private final VersionNumber eaVersion; + private final Additional additional; - Release(String value, int major, int minor, int patch, int eaType, VersionNumber eaVersion) { - super(value); + Release(String value, String normalized, int major, int minor, int patch, ReleaseType eaType, VersionNumber eaVersion, Additional additional) { + super(value, normalized); this.major = major; this.minor = minor; this.patch = patch; this.eaType = eaType; this.eaVersion = eaVersion; + this.additional = additional; } @Override @@ -367,35 +459,45 @@ public abstract sealed class GameVersionNumber implements Comparable= 0) - return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1; - - idx = -(idx + 1); - if (idx == Versions.SNAPSHOT_INTS.length) + int compareToSnapshot(LegacySnapshot other) { + if (major == 0) { return -1; + } else if (major == 1) { + int idx = Arrays.binarySearch(Versions.SNAPSHOT_INTS, other.intValue); + if (idx >= 0) + return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1; - return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1; + idx = -(idx + 1); + if (idx == Versions.SNAPSHOT_INTS.length) + return -1; + + return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1; + } else { + return 1; + } } @Override int compareToImpl(@NotNull GameVersionNumber other) { - if (other instanceof Release) - return compareToRelease((Release) other); + if (other instanceof Release release) + return compareToRelease(release); - if (other instanceof Snapshot) - return compareToSnapshot((Snapshot) other); + if (other instanceof LegacySnapshot snapshot) + return compareToSnapshot(snapshot); - if (other instanceof Special) - return -((Special) other).compareToReleaseOrSnapshot(this); + if (other instanceof Special special) + return -special.compareToReleaseOrSnapshot(this); throw new AssertionError(other.getClass()); } @@ -412,7 +514,7 @@ public abstract sealed class GameVersionNumber implements Comparable 'z') && suffix != '~') + if (suffix < 'a' || suffix > 'z') throw new IllegalArgumentException(value); - return new Snapshot(value, year, week, suffix); + return new LegacySnapshot(value, normalized, year, week, suffix, unobfuscated); } - static int toInt(int year, int week, char suffix) { - return (year << 16) | (week << 8) | suffix; + static int toInt(int year, int week, char suffix, boolean unobfuscated) { + return (year << 24) | (week << 16) | (suffix << 8) | (unobfuscated ? 1 : 0); } final int intValue; - Snapshot(String value, int year, int week, char suffix) { - super(value); - this.intValue = toInt(year, week, suffix); + LegacySnapshot(String value, String normalized, int year, int week, char suffix, boolean unobfuscated) { + super(value, normalized); + this.intValue = toInt(year, week, suffix, unobfuscated); } @Override @@ -476,40 +615,52 @@ public abstract sealed class GameVersionNumber implements Comparable> 16) & 0xff; + return (intValue >> 24) & 0xff; } public int getWeek() { - return (intValue >> 8) & 0xff; + return (intValue >> 16) & 0xff; } public char getSuffix() { - return (char) (intValue & 0xff); + return (char) ((intValue >> 8) & 0xff); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - return o instanceof Snapshot other && this.intValue == other.intValue; + public boolean isUnobfuscated() { + return (intValue & 0b00000001) != 0; } @Override public int hashCode() { return intValue; } + + @Override + public boolean equals(Object o) { + return o instanceof LegacySnapshot that && this.intValue == that.intValue; + } + + @Override + protected ToStringBuilder buildDebugString() { + return super.buildDebugString() + .append("year", getYear()) + .append("week", getWeek()) + .append("suffix", getSuffix()) + .append("unobfuscated", isUnobfuscated()); + } } public static final class Special extends GameVersionNumber { @@ -517,8 +668,8 @@ public abstract sealed class GameVersionNumber implements Comparable defaultGameVersions = new ArrayDeque<>(64); - List snapshots = new ArrayList<>(1024); + List snapshots = new ArrayList<>(1024); List snapshotPrev = new ArrayList<>(1024); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.US_ASCII))) { + //noinspection DataFlowIssue + try (var reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.US_ASCII))) { Release currentRelease = null; GameVersionNumber prev = null; @@ -637,13 +785,13 @@ public abstract sealed class GameVersionNumber implements Comparable readVersions() { List versions = new ArrayList<>(); @@ -48,18 +51,8 @@ public final class GameVersionNumberTest { return versions; } - @Test - public void testSortVersions() { - List versions = readVersions(); - List copied = new ArrayList<>(versions); - Collections.shuffle(copied, new Random(0)); - copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion)); - - assertIterableEquals(versions, copied); - } - - private static String errorMessage(String version1, String version2) { - return String.format("version1=%s, version2=%s", version1, version2); + private static Supplier errorMessage(GameVersionNumber version1, GameVersionNumber version2) { + return () -> "version1=%s, version2=%s".formatted(version1.toDebugString(), version2.toDebugString()); } private static void assertGameVersionEquals(String version) { @@ -67,25 +60,42 @@ public final class GameVersionNumberTest { } private static void assertGameVersionEquals(String version1, String version2) { - assertEquals(0, asGameVersion(version1).compareTo(version2), errorMessage(version1, version2)); - assertEquals(asGameVersion(version1), asGameVersion(version2), errorMessage(version1, version2)); - } - - private static String toString(GameVersionNumber gameVersionNumber) { - return gameVersionNumber.getClass().getSimpleName(); + GameVersionNumber gameVersion1 = asGameVersion(version1); + GameVersionNumber gameVersion2 = asGameVersion(version2); + assertEquals(0, gameVersion1.compareTo(gameVersion2), errorMessage(gameVersion1, gameVersion2)); + assertEquals(0, gameVersion2.compareTo(gameVersion1), errorMessage(gameVersion1, gameVersion2)); + assertEquals(gameVersion1, gameVersion2, errorMessage(gameVersion1, gameVersion2)); + assertEquals(gameVersion2, gameVersion1, errorMessage(gameVersion1, gameVersion2)); + assertEquals(gameVersion1.hashCode(), gameVersion2.hashCode(), errorMessage(gameVersion1, gameVersion2)); } private static void assertOrder(String... versions) { + var gameVersionNumbers = new GameVersionNumber[versions.length]; + for (int i = 0; i < versions.length; i++) { + gameVersionNumbers[i] = asGameVersion(versions[i]); + } + for (int i = 0; i < versions.length - 1; i++) { - GameVersionNumber version1 = asGameVersion(versions[i]); + GameVersionNumber version1 = gameVersionNumbers[i]; + + for (int j = 0; j < i; j++) { + GameVersionNumber version2 = gameVersionNumbers[j]; + + assertTrue(version1.compareTo(version2) > 0, errorMessage(version1, version2)); + assertTrue(version2.compareTo(version1) < 0, errorMessage(version1, version2)); + assertNotEquals(version1, version2, errorMessage(version1, version2)); + assertNotEquals(version2, version1, errorMessage(version1, version2)); + } assertGameVersionEquals(versions[i]); for (int j = i + 1; j < versions.length; j++) { - GameVersionNumber version2 = asGameVersion(versions[j]); + GameVersionNumber version2 = gameVersionNumbers[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))); + assertTrue(version1.compareTo(version2) < 0, errorMessage(version1, version2)); + assertTrue(version2.compareTo(version1) > 0, errorMessage(version1, version2)); + assertNotEquals(version1, version2, errorMessage(version1, version2)); + assertNotEquals(version2, version1, errorMessage(version1, version2)); } } @@ -100,6 +110,8 @@ public final class GameVersionNumberTest { assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber); } + //endregion Helpers + private static boolean isAprilFools(String version) { return asGameVersion(version).isAprilFools(); } @@ -124,6 +136,31 @@ public final class GameVersionNumberTest { assertFalse(isAprilFools("25w45a_unobfuscated")); } + @Test + public void testSortVersions() { + List versions = readVersions(); + + { + List copied = new ArrayList<>(versions); + copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion)); + assertIterableEquals(versions, copied); + } + + { + List copied = new ArrayList<>(versions); + Collections.reverse(copied); + copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion)); + assertIterableEquals(versions, copied); + } + + for (int randomSeed = 0; randomSeed < 5; randomSeed++) { + List copied = new ArrayList<>(versions); + Collections.shuffle(copied, new Random(randomSeed)); + copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion)); + assertIterableEquals(versions, copied); + } + } + @Test public void testParseOld() { assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211"); @@ -136,36 +173,96 @@ public final class GameVersionNumberTest { assertOldVersion("a1.0.13_01-1", GameVersionNumber.Type.ALPHA, "1.0.13_01-1"); assertOldVersion("b1.0", GameVersionNumber.Type.BETA, "1.0"); assertOldVersion("b1.0_01", GameVersionNumber.Type.BETA, "1.0_01"); + assertOldVersion("b1.6-tb3", GameVersionNumber.Type.BETA, "1.6-tb3"); assertOldVersion("b1.8-pre1-2", GameVersionNumber.Type.BETA, "1.8-pre1-2"); assertOldVersion("b1.9-pre1", GameVersionNumber.Type.BETA, "1.9-pre1"); + + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("1.21")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("r-132211")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("rd-")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("rd-a")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("i-20100223")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("in-")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("in-a")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("inf-")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("inf-a")); + } + + private static void testParseLegacySnapshot(int year, int week, char suffix) { + String raw = "%02dw%02d%s".formatted(year, week, suffix); + var rawVersion = (GameVersionNumber.LegacySnapshot) asGameVersion(raw); + assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion); + assertEquals(raw, rawVersion.toString()); + assertEquals(raw, rawVersion.toNormalizedString()); + assertEquals(year, rawVersion.getYear()); + assertEquals(week, rawVersion.getWeek()); + assertEquals(suffix, rawVersion.getSuffix()); + assertFalse(rawVersion.isUnobfuscated()); + + var unobfuscated = raw + "_unobfuscated"; + var unobfuscatedVersion = (GameVersionNumber.LegacySnapshot) asGameVersion(unobfuscated); + assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion); + assertEquals(unobfuscated, unobfuscatedVersion.toString()); + assertEquals(unobfuscated, unobfuscatedVersion.toNormalizedString()); + assertEquals(year, unobfuscatedVersion.getYear()); + assertEquals(week, unobfuscatedVersion.getWeek()); + assertEquals(suffix, unobfuscatedVersion.getSuffix()); + assertTrue(unobfuscatedVersion.isUnobfuscated()); + + var unobfuscated2 = raw + " Unobfuscated"; + var unobfuscatedVersion2 = (GameVersionNumber.LegacySnapshot) asGameVersion(unobfuscated2); + assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion); + assertEquals(unobfuscated2, unobfuscatedVersion2.toString()); + assertEquals(unobfuscated, unobfuscatedVersion2.toNormalizedString()); + assertEquals(year, unobfuscatedVersion2.getYear()); + assertEquals(week, unobfuscatedVersion2.getWeek()); + assertEquals(suffix, unobfuscatedVersion2.getSuffix()); + assertTrue(unobfuscatedVersion2.isUnobfuscated()); } @Test public void testParseNew() { List versions = readVersions(); for (String version : versions) { - assertFalse(asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version); + GameVersionNumber gameVersion = asGameVersion(version); + assertFalse(gameVersion instanceof GameVersionNumber.Old, "version=" + gameVersion.toDebugString()); } + + testParseLegacySnapshot(25, 46, 'a'); + + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parse("2.1")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("1.0")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("1.100.1")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("aawbba")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("13w12A")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("13w12~")); } - private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int minor, int patch) { + private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int major, int minor, int patch) { GameVersionNumber.Release release = GameVersionNumber.Release.parseSimple(simpleReleaseVersion); assertAll("Assert Simple Release Version " + simpleReleaseVersion, - () -> assertEquals(1, release.getMajor()), + () -> assertEquals(major, release.getMajor()), () -> assertEquals(minor, release.getMinor()), () -> assertEquals(patch, release.getPatch()), - () -> assertEquals(GameVersionNumber.Release.TYPE_UNKNOWN, release.getEaType()), + () -> assertEquals(GameVersionNumber.Release.ReleaseType.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); + assertSimpleReleaseVersion("1.0", 1, 0, 0); + assertSimpleReleaseVersion("1.13", 1, 13, 0); + assertSimpleReleaseVersion("1.21.8", 1, 21, 8); + assertSimpleReleaseVersion("26.1", 26, 1, 0); + assertSimpleReleaseVersion("26.1.1", 26, 1, 1); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("26")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("24.0.0")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("24.0")); assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("2.0")); + assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1")); assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1..0")); assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0.")); assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.a")); @@ -186,13 +283,13 @@ public final class GameVersionNumberTest { "0.0", "1.0", "1.99", - "1.99.1-unknown1", "1.99.1-pre1", "1.99.1 Pre-Release 2", "1.99.1-rc1", "1.99.1", "1.100", - "1.100.1" + "1.100.1", + "26.1" ); } @@ -202,7 +299,6 @@ public final class GameVersionNumberTest { "90w01a", "90w01b", "90w01e", - "90w01~", "90w02a" ); } @@ -257,6 +353,7 @@ public final class GameVersionNumberTest { "1.14", "1.15.2", "20w06a", + "20w13b", "20w14infinite", "20w22a", "1.16-pre1", @@ -273,10 +370,21 @@ public final class GameVersionNumberTest { "24w13a", "24w14potato", "24w14a", - "25w45a", - "25w45a_unobfuscated", - "Unknown", - "100.0" + "25w46a", + "25w46a_unobfuscated", + "1.21.11-pre1", + "1.21.11-pre1_unobfuscated", + "1.21.11-pre2", + "1.21.11-pre2_unobfuscated", + "99w99a", + "26.1-snapshot-1", + "26.1-snapshot-2", + "26.1", + "26.2-snapshot-1", + "26.2-snapshot-2", + "26.2", + "100.0", + "Unknown" ); } @@ -312,26 +420,83 @@ public final class GameVersionNumberTest { ); } + private static void assertNormalized(String normalized, String version) { + assertGameVersionEquals(version); + assertGameVersionEquals(normalized, version); + assertEquals(normalized, asGameVersion(version).toNormalizedString()); + } + + @Test + public void testToNormalizedString() { + for (String version : readVersions()) { + assertNormalized(version, version); + } + + assertNormalized("1.21.11-pre3", "1.21.11 Pre-Release 3"); + assertNormalized("1.21.11-pre3_unobfuscated", "1.21.11 Pre-Release 3 Unobfuscated"); + assertNormalized("1.21.11-pre3_unobfuscated", "1.21.11-pre3 Unobfuscated"); + assertNormalized("1.21.11-rc1", "1.21.11 Release Candidate 1"); + assertNormalized("1.21.11-rc1_unobfuscated", "1.21.11 Release Candidate 1 Unobfuscated"); + assertNormalized("1.14_combat-212796", "1.14.3 - Combat Test"); + assertNormalized("1.14_combat-0", "Combat Test 2"); + assertNormalized("1.14_combat-3", "Combat Test 3"); + assertNormalized("1.15_combat-1", "Combat Test 4"); + assertNormalized("1.15_combat-6", "Combat Test 5"); + assertNormalized("1.16_combat-0", "Combat Test 6"); + assertNormalized("1.16_combat-1", "Combat Test 7"); + assertNormalized("1.16_combat-2", "Combat Test 7b"); + assertNormalized("1.16_combat-3", "Combat Test 7c"); + assertNormalized("1.16_combat-4", "Combat Test 8"); + assertNormalized("1.16_combat-5", "Combat Test 8b"); + assertNormalized("1.16_combat-6", "Combat Test 8c"); + assertNormalized("1.18_experimental-snapshot-1", "1.18 Experimental Snapshot 1"); + assertNormalized("1.18_experimental-snapshot-2", "1.18 Experimental Snapshot 2"); + assertNormalized("1.18_experimental-snapshot-3", "1.18 Experimental Snapshot 3"); + assertNormalized("1.18_experimental-snapshot-4", "1.18 Experimental Snapshot 4"); + assertNormalized("1.18_experimental-snapshot-5", "1.18 Experimental Snapshot 5"); + assertNormalized("1.18_experimental-snapshot-6", "1.18 Experimental Snapshot 6"); + assertNormalized("1.19_deep_dark_experimental_snapshot-1", "Deep Dark Experimental Snapshot 1"); + assertNormalized("20w14infinite", "20w14~"); + } + @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")); + assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a", false)); + assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a", true)); + assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a", false)); - 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")); + assertFalse(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a", false)); + assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a", false)); + assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a", false)); + assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a", false)); + assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a", false)); + assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a", true)); + assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a", false)); + + assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a", true)); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a", false)); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13", true)); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13", false)); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", true)); + assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", false)); } }