diff --git a/package-lock.json b/package-lock.json index 7748192..c9dc312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2262,6 +2262,11 @@ } } }, + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -2711,6 +2716,14 @@ "bser": "2.1.1" } }, + "fetch-blob": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz", + "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==", + "requires": { + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3910,6 +3923,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", + "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^3.1.2" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4784,6 +4806,11 @@ "makeerror": "1.0.x" } }, + "web-streams-polyfill": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", + "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==" + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/package.json b/package.json index d923a12..d145c7c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@actions/core": "^1.5.0", - "fast-glob": "^3.2.7" + "fast-glob": "^3.2.7", + "node-fetch": "^3.0.0" } } diff --git a/src/utils/minecraft-utils.ts b/src/utils/minecraft-utils.ts new file mode 100644 index 0000000..4037f5c --- /dev/null +++ b/src/utils/minecraft-utils.ts @@ -0,0 +1,153 @@ +import fetch from "node-fetch"; +import Version from "./version"; + +export enum MinecraftVersionType { + Release = "release", + Snapshot = "snapshot", + OldBeta = "old_beta", + OldAlpha = "old_alpha" +} + +export class MinecraftVersion { + public readonly id: string; + public readonly name: string; + public readonly version: Version; + public readonly type: MinecraftVersionType; + public readonly url: string; + public readonly time: Date; + public readonly releaseTime: Date; + + public constructor(id: string, name: string, type: MinecraftVersionType, url: string, time: Date, releaseTime: Date) { + this.id = id; + this.name = name; + this.version = new Version(name); + this.type = type; + this.url = url; + this.time = time; + this.releaseTime = releaseTime; + } + + public get isRelease(): boolean { + return this.type === MinecraftVersionType.Release; + } + + public get isSnapshot(): boolean { + return this.type === MinecraftVersionType.Snapshot; + } +} + +interface ParsedMinecraftVersion { + id: string; + type: MinecraftVersionType; + url: string; + time: string; + releaseTime: string; +} + +let cachedVersionsById: Map = null; +async function getVersionMap(): Promise> { + if (!cachedVersionsById) { + cachedVersionsById = await loadVersions(); + } + return cachedVersionsById; +} + +async function loadVersions(): Promise> { + const response = <{ versions: ParsedMinecraftVersion[] }>await (await fetch("https://launchermeta.mojang.com/mc/game/version_manifest.json")).json(); + const versionsById = new Map(); + for (let i = 0; i < response.versions.length; ++i) { + const version = response.versions[i]; + versionsById.set(version.id, new MinecraftVersion(version.id, getNearestReleaseVersionName(response.versions, i), version.type, version.url, new Date(version.time), new Date(version.releaseTime))); + } + return versionsById; +} + +function getNearestReleaseVersionName(versions: ParsedMinecraftVersion[], start: number): string { + for (let i = start; i >= 0; --i) { + if (versions[i].type === MinecraftVersionType.Release) { + return versions[i].id; + } + } + + const versionMatch = versions[start].id.match(/\d+\.\d+(?:\.\d+)?/); + if (versionMatch && versionMatch.length > 0) { + return versionMatch[0]; + } + + for (let i = start + 1; i < versions.length; ++i) { + if (versions[i].type === MinecraftVersionType.Release) { + return extractVersion(versions[i].id).split(".").map((x, i) => i === 1 ? (+x + 1) : x).filter((x, i) => i < 2).join("."); + } + } + + return null; +} + +export async function getVersions(): Promise { + return [...(await getVersionMap()).values()]; +} + +export async function getVersionById(id: string): Promise { + return (await getVersionMap()).get(id.trim()) || null; +} + +export async function findVersionByName(name: string, snapshot?: boolean): Promise { + const versionMap = await getVersionMap(); + snapshot ??= isSnapshot(name); + const foundVersion = versionMap.get(name); + if (foundVersion && foundVersion.isSnapshot === !!snapshot) { + return foundVersion; + } + + name = extractVersion(name); + for (const version of versionMap.values()) { + if (version.name === name && version.isSnapshot) { + return version; + } + } + return null; +} + +function extractVersion(versionName: string): string { + return versionName.match(/(? x.match(/\d+\.\d+(?:\.\d+)?/)).filter(x => x).map(x => x[0]); + return versionCandidates.length > 1 ? versionCandidates.filter(x => x.startsWith("1.")).reverse()[0] : null; + } +} + +export async function getLatestRelease(): Promise { + return (await getVersions()).find(x => x.isRelease) || null; +} + +export async function getCompatibleBuilds(build: string | Version): Promise { + if (!(build instanceof Version)) { + build = new Version(build); + } + const versions = new Array(); + for (const version of await getVersions()) { + if (version.version.major !== build.major) { + continue; + } + + if (version.version.minor < build.minor) { + break; + } + + if (version.version.minor === build.minor && version.version.build >= build.build) { + versions.push(version); + } + } + return versions; +} diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..ff118a7 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,19 @@ +export default class Version { + public readonly major: number; + public readonly minor: number; + public readonly build: number; + + public constructor(major: number, minor: number, build: number); + + public constructor(version: string); + + public constructor(major: number | string, minor?: number, build?: number) { + if (typeof major === "string") { + [this.major, this.minor, this.build] = major.split(".").map(x => isNaN(+x) ? 0 : +x).concat(0, 0); + } else { + this.major = major || 0; + this.minor = minor || 0; + this.build = build || 0; + } + } +}