mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 00:11:02 -05:00
Implemented Minecraft version normalization scheme
~~Stolen~~ Adapted from FabricMC
This commit is contained in:
parent
14c9a183bc
commit
6eb5672a79
1 changed files with 409 additions and 0 deletions
409
src/games/minecraft/minecraft-version-lookup.ts
Normal file
409
src/games/minecraft/minecraft-version-lookup.ts
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
import { asArrayLike, isIterable } from "@/utils/collections";
|
||||||
|
import { VersionRange, parseVersionRange } from "@/utils/versioning";
|
||||||
|
import { MinecraftVersion, MinecraftVersionManifestEntry } from "./minecraft-version";
|
||||||
|
import { MinecraftVersionType } from "./minecraft-version-type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regular expression pattern to match various Minecraft version strings.
|
||||||
|
*/
|
||||||
|
const VERSION_PATTERN = (
|
||||||
|
"0\\.\\d+(?:\\.\\d+)?a?(?:_\\d+)?|" +
|
||||||
|
"\\d+\\.\\d+(?:\\.\\d+)?(?:-pre\\d+| Pre-[Rr]elease \\d+|-rc\\d+| [Rr]elease Candidate \\d+)?|" +
|
||||||
|
"\\d+w\\d+(?:[a-z]+|~)|" +
|
||||||
|
"[a-c]\\d\\.\\d+(?:\\.\\d+)?[a-z]?(?:_\\d+)?[a-z]?|" +
|
||||||
|
"(Alpha|Beta) v?\\d+\\.\\d+(?:\\.\\d+)?[a-z]?(?:_\\d+)?[a-z]?|" +
|
||||||
|
"Inf?dev (?:0\\.31 )?\\d+(?:-\\d+)?|" +
|
||||||
|
"(?:rd|inf)-\\d+|" +
|
||||||
|
"(?:.*[Ee]xperimental [Ss]napshot )(?:\\d+)"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating Minecraft version strings.
|
||||||
|
*/
|
||||||
|
const VERSION_REGEX = new RegExp(VERSION_PATTERN);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating release Minecraft versions.
|
||||||
|
*/
|
||||||
|
const RELEASE_REGEX = /\d+\.\d+(\.\d+)?/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating pre-release Minecraft versions.
|
||||||
|
*/
|
||||||
|
const PRE_RELEASE_REGEX = /.+(?:-pre| Pre-[Rr]elease )(\d+)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating release candidate Minecraft versions.
|
||||||
|
*/
|
||||||
|
const RELEASE_CANDIDATE_REGEX = /.+(?:-rc| [Rr]elease Candidate )(\d+)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating snapshot Minecraft versions.
|
||||||
|
*/
|
||||||
|
const SNAPSHOT_REGEX = /(?:Snapshot )?(\d+)w0?(0|[1-9]\d*)([a-z])/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating experimental snapshot Minecraft versions.
|
||||||
|
*/
|
||||||
|
const EXPERIMENTAL_REGEX = /(?:.*[Ee]xperimental [Ss]napshot )(\d+)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating beta Minecraft versions.
|
||||||
|
*/
|
||||||
|
const BETA_REGEX = /(?:b|Beta v?)1\.(\d+(\.\d+)?[a-z]?(_\d+)?[a-z]?)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating alpha Minecraft versions.
|
||||||
|
*/
|
||||||
|
const ALPHA_REGEX = /(?:a|Alpha v?)[01]\.(\d+(\.\d+)?[a-z]?(_\d+)?[a-z]?)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for matching and validating in-development Minecraft versions.
|
||||||
|
*/
|
||||||
|
const INDEV_REGEX = /(?:inf-|Inf?dev )(?:0\.31 )?(\d+(-\d+)?)/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the range of legacy Minecraft versions.
|
||||||
|
*
|
||||||
|
* It is used to determine if a given Minecraft version string is considered a legacy version or not.
|
||||||
|
* In our case, versions less than or equal to `1.16` are considered legacy.
|
||||||
|
*/
|
||||||
|
const LEGACY_VERSION_RANGE = parseVersionRange("<=1.16");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of special Minecraft versions (e.g., April Fools' ones) and their normalized counterparts.
|
||||||
|
*/
|
||||||
|
const SPECIAL_VERSIONS: ReadonlyMap<string, string> = new Map([
|
||||||
|
["13w12~", "1.5.1-alpha.13.12.a"],
|
||||||
|
["2point0_red", "1.5.2-red"],
|
||||||
|
["2point0_purple", "1.5.2-purple"],
|
||||||
|
["2point0_blue", "1.5.2-blue"],
|
||||||
|
["15w14a", "1.8.4-alpha.15.14.a+loveandhugs"],
|
||||||
|
["1.RV-Pre1", "1.9.2-rv+trendy"],
|
||||||
|
["3D Shareware v1.34", "1.14-alpha.19.13.shareware"],
|
||||||
|
["1.14.3 - Combat Test", "1.14.3-rc.4.combat.1"],
|
||||||
|
["Combat Test 2", "1.14.5-combat.2"],
|
||||||
|
["Combat Test 3", "1.14.5-combat.3"],
|
||||||
|
["Combat Test 4", "1.15-rc.3.combat.4"],
|
||||||
|
["Combat Test 5", "1.15.2-rc.2.combat.5"],
|
||||||
|
["20w14~", "1.16-alpha.20.13.inf"],
|
||||||
|
["Combat Test 6", "1.16.2-beta.3.combat.6"],
|
||||||
|
["Combat Test 7", "1.16.3-combat.7"],
|
||||||
|
["1.16_combat-2", "1.16.3-combat.7.b"],
|
||||||
|
["1.16_combat-3", "1.16.3-combat.7.c"],
|
||||||
|
["1.16_combat-4", "1.16.3-combat.8"],
|
||||||
|
["1.16_combat-5", "1.16.3-combat.8.b"],
|
||||||
|
["1.16_combat-6", "1.16.3-combat.8.c"],
|
||||||
|
["23w13a_or_b", "1.20-alpha.23.13.ab"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a given Minecraft version string.
|
||||||
|
*
|
||||||
|
* @param version - The Minecraft version string to normalize.
|
||||||
|
* @param versions - Optional Minecraft version manifest entries.
|
||||||
|
* @param index - Optional index of the Minecraft version in the manifest entries.
|
||||||
|
*
|
||||||
|
* @returns The normalized Minecraft version string.
|
||||||
|
*/
|
||||||
|
export function normalizeMinecraftVersion(version: string, versions?: MinecraftVersionManifestEntry[], index?: number): string {
|
||||||
|
const releaseVersion = versions ? findNearestReleaseMinecraftVersion(versions, index) : version.match(RELEASE_REGEX)?.[0];
|
||||||
|
return normalizeUnknownMinecraftVersion(version, releaseVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a Minecraft version range.
|
||||||
|
*
|
||||||
|
* @param range - The version range to normalize.
|
||||||
|
* @param versions - A map of Minecraft versions and their corresponding ids.
|
||||||
|
* @param versionRegex - A regular expression for matching Minecraft versions.
|
||||||
|
*
|
||||||
|
* @returns The normalized Minecraft version range.
|
||||||
|
*/
|
||||||
|
export function normalizeMinecraftVersionRange(range: string | Iterable<string> | VersionRange, versions: ReadonlyMap<string, MinecraftVersion>, versionRegex: RegExp): VersionRange {
|
||||||
|
if (!isIterable(range)) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = typeof range === "string" ? [range] : asArrayLike(range);
|
||||||
|
const normalizedRanges = ranges.map((r: string) => r.replaceAll(versionRegex, x => {
|
||||||
|
const version = versions.get(x);
|
||||||
|
if (version) {
|
||||||
|
return String(version.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeMinecraftVersion(x);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return parseVersionRange(normalizedRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a regular expression for matching Minecraft versions.
|
||||||
|
*
|
||||||
|
* @param versions - Optional collection of Minecraft versions that should satisfy the resulting regex.
|
||||||
|
*
|
||||||
|
* @returns A regular expression for matching Minecraft versions.
|
||||||
|
*/
|
||||||
|
export function getMinecraftVersionRegExp(versions?: Iterable<string>): RegExp {
|
||||||
|
if (!versions) {
|
||||||
|
return VERSION_REGEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern = VERSION_PATTERN;
|
||||||
|
for (const version of versions) {
|
||||||
|
if (version.match(VERSION_REGEX)?.[0] !== version) {
|
||||||
|
pattern = `${version.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d")}|${pattern}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pattern === VERSION_PATTERN ? VERSION_REGEX : new RegExp(pattern, "gs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an unknown Minecraft version.
|
||||||
|
*
|
||||||
|
* The normalization process formats the version string to provide better compatibility with
|
||||||
|
* FabricMC's normalization scheme. This may involve appending the release version, converting
|
||||||
|
* snapshot, experimental, or pre-release information, or transforming old version strings.
|
||||||
|
*
|
||||||
|
* @param version - The Minecraft version string to normalize.
|
||||||
|
* @param releaseVersion - Optional release version string for context.
|
||||||
|
*
|
||||||
|
* @returns The normalized Minecraft version string.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Original algorithm from FabricMC:
|
||||||
|
* https://github.com/FabricMC/fabric-loader/blob/HEAD/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java
|
||||||
|
*/
|
||||||
|
function normalizeUnknownMinecraftVersion(version: string, releaseVersion?: string): string {
|
||||||
|
if (SPECIAL_VERSIONS.has(version)) {
|
||||||
|
return SPECIAL_VERSIONS.get(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!releaseVersion || version === releaseVersion) {
|
||||||
|
return normalizeOldMinecraftVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let match: RegExpMatchArray;
|
||||||
|
if (match = version.match(EXPERIMENTAL_REGEX)) {
|
||||||
|
return `${releaseVersion}-Experimental.${match[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.startsWith(releaseVersion)) {
|
||||||
|
if (match = version.match(RELEASE_CANDIDATE_REGEX)) {
|
||||||
|
const rcBuild = releaseVersion === "1.16" ? String(8 + (+match[1])) : match[1];
|
||||||
|
version = `rc.${rcBuild}`;
|
||||||
|
} else if (match = version.match(PRE_RELEASE_REGEX)) {
|
||||||
|
const isLegacy = isLegacyMinecraftVersion(releaseVersion);
|
||||||
|
version = `${isLegacy ? "rc" : "beta"}.${match[1]}`;
|
||||||
|
}
|
||||||
|
} else if (match = version.match(SNAPSHOT_REGEX)) {
|
||||||
|
version = `alpha.${match[1]}.${match[2]}.${match[3]}`;
|
||||||
|
} else {
|
||||||
|
version = normalizeOldMinecraftVersion(version);
|
||||||
|
}
|
||||||
|
return `${releaseVersion}-${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an old Minecraft version by converting version components like alpha, beta,
|
||||||
|
* and indev to a more standard format, as well as removing unnecessary characters and correcting
|
||||||
|
* the separator placements.
|
||||||
|
*
|
||||||
|
* @param version - The old Minecraft version string to normalize.
|
||||||
|
*
|
||||||
|
* @returns The normalized Minecraft version string.
|
||||||
|
*/
|
||||||
|
function normalizeOldMinecraftVersion(version: string): string {
|
||||||
|
let matcher: RegExpMatchArray;
|
||||||
|
if (matcher = version.match(BETA_REGEX)) {
|
||||||
|
version = `1.0.0-beta.${matcher[1]}`;
|
||||||
|
} else if (matcher = version.match(ALPHA_REGEX)) {
|
||||||
|
version = `1.0.0-alpha.${matcher[1]}`;
|
||||||
|
} else if (matcher = version.match(INDEV_REGEX)) {
|
||||||
|
version = `0.31.${matcher[1]}`;
|
||||||
|
} else if (version.startsWith("c0.")) {
|
||||||
|
version = version.substring(1);
|
||||||
|
} else if (version.startsWith("rd-")) {
|
||||||
|
version = version.substring(3);
|
||||||
|
if (version === "20090515") {
|
||||||
|
version = "150000";
|
||||||
|
}
|
||||||
|
version = `0.0.0-rd.${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = "";
|
||||||
|
let wasDigit = false;
|
||||||
|
let wasLeadingZero = false;
|
||||||
|
let wasSeparator = false;
|
||||||
|
let hasHyphen = false;
|
||||||
|
for (let i = 0; i < version.length; ++i) {
|
||||||
|
let c = version.charAt(i);
|
||||||
|
if (c >= "0" && c <= "9") {
|
||||||
|
if (i > 0 && !wasDigit && !wasSeparator) {
|
||||||
|
normalized += ".";
|
||||||
|
} else if (wasDigit && wasLeadingZero) {
|
||||||
|
normalized = normalized.substring(0, normalized.length - 1);
|
||||||
|
}
|
||||||
|
wasLeadingZero = c === "0" && (!wasDigit || wasLeadingZero);
|
||||||
|
wasSeparator = false;
|
||||||
|
wasDigit = true;
|
||||||
|
} else if (c === "." || c === "-") {
|
||||||
|
if (wasSeparator) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasSeparator = true;
|
||||||
|
wasDigit = false;
|
||||||
|
} else if ((c < "A" || c > "Z") && (c < "a" || c > "z")) {
|
||||||
|
if (wasSeparator) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = ".";
|
||||||
|
wasSeparator = true;
|
||||||
|
wasDigit = false;
|
||||||
|
} else {
|
||||||
|
if (wasDigit) {
|
||||||
|
normalized += hasHyphen ? "." : "-";
|
||||||
|
hasHyphen = true;
|
||||||
|
}
|
||||||
|
wasSeparator = false;
|
||||||
|
wasDigit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === "-") {
|
||||||
|
hasHyphen = true;
|
||||||
|
}
|
||||||
|
normalized += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = 0;
|
||||||
|
while (start < normalized.length && normalized.charAt(start) === ".") {
|
||||||
|
++start;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = normalized.length;
|
||||||
|
while (end > start && normalized.charAt(end - 1) === ".") {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the nearest release Minecraft version to a given index in the provided version manifest entries.
|
||||||
|
*
|
||||||
|
* This is used to determine the release version context for non-release versions (e.g., snapshots).
|
||||||
|
*
|
||||||
|
* @param versions - An array of Minecraft version manifest entries.
|
||||||
|
* @param index - The index of the version for which to find the nearest release version.
|
||||||
|
*
|
||||||
|
* @returns The nearest release Minecraft version string, or `undefined` if not found.
|
||||||
|
*/
|
||||||
|
function findNearestReleaseMinecraftVersion(versions: MinecraftVersionManifestEntry[], index: number): string | undefined {
|
||||||
|
if (versions[index].type === MinecraftVersionType.RELEASE) {
|
||||||
|
return versions[index].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versions[index].type !== MinecraftVersionType.SNAPSHOT) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = versions[index].id.match(RELEASE_REGEX);
|
||||||
|
if (match) {
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = versions[index].id.match(SNAPSHOT_REGEX);
|
||||||
|
if (snapshot) {
|
||||||
|
const year = +snapshot[1];
|
||||||
|
const week = +snapshot[2];
|
||||||
|
|
||||||
|
const hardcodedSnapshotVersion = findNearestReleaseMinecraftVersionBySnapshotDate(year, week);
|
||||||
|
if (hardcodedSnapshotVersion) {
|
||||||
|
return hardcodedSnapshotVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = index - 1; i >= 0; --i) {
|
||||||
|
if (versions[i].type === MinecraftVersionType.RELEASE) {
|
||||||
|
return versions[i].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = index + 1; i < versions.length; ++i) {
|
||||||
|
if (versions[i].type !== MinecraftVersionType.RELEASE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = versions[i].id.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
||||||
|
if (match) {
|
||||||
|
return `${match[1]}.${match[2]}.${(+match[3] || 0) + 1}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the nearest release Minecraft version based on the snapshot year and week.
|
||||||
|
*
|
||||||
|
* This function is required because the order of versions in the version manifest may not
|
||||||
|
* always correspond to their actual release order, especially for older versions.
|
||||||
|
* By using hardcoded release versions for specific date ranges, we can determine the nearest
|
||||||
|
* release version more accurately for certain snapshots.
|
||||||
|
*
|
||||||
|
* @param year - The snapshot year.
|
||||||
|
* @param week - The snapshot week.
|
||||||
|
*
|
||||||
|
* @returns The nearest release Minecraft version string, or `undefined` if not found.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Original algorithm from FabricMC:
|
||||||
|
* https://github.com/FabricMC/fabric-loader/blob/HEAD/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java#L267
|
||||||
|
*/
|
||||||
|
function findNearestReleaseMinecraftVersionBySnapshotDate(year: number, week: number) : string | undefined {
|
||||||
|
if (year === 23 && week >= 12) {
|
||||||
|
return "1.20";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 20 && week >= 45 || year === 21 && week <= 20) {
|
||||||
|
return "1.17";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 15 && week >= 31 || year === 16 && week <= 7) {
|
||||||
|
return "1.9";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 14 && week >= 2 && week <= 34) {
|
||||||
|
return "1.8";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 13 && week >= 47 && week <= 49) {
|
||||||
|
return "1.7.4";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 13 && week >= 36 && week <= 43) {
|
||||||
|
return "1.7.2";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (year === 13 && week >= 16 && week <= 26) {
|
||||||
|
return "1.6";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a Minecraft version is considered legacy based on its version string.
|
||||||
|
*
|
||||||
|
* @param version - The Minecraft version string to evaluate.
|
||||||
|
*
|
||||||
|
* @returns `true` if the version is considered legacy; otherwise, `false`.
|
||||||
|
*/
|
||||||
|
function isLegacyMinecraftVersion(version: string): boolean {
|
||||||
|
return LEGACY_VERSION_RANGE.includes(version);
|
||||||
|
}
|
Loading…
Reference in a new issue