import json import logging import warnings from pathlib import Path import requests from ..common.common import FileObject from ..common.exception import VersionManifestFetchException, NoVersionFoundException, \ NoSpecifiedVersionKeyException, VersionDataKeyNotFoundException, VersionManifestSaveException, HashMismatchException MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" MOJANG_VERSION_MANIFEST = "https://piston-meta.mojang.com/mc/game/version_manifest.json" MOJANG_LIBRARIES_ENDPOINT = "https://libraries.minecraft.net/" NATIVE_OS_KEY_MAP = { "darwin": "macos", "macos": "macos", "osx": "macos", "linux": "linux", "windows": "windows", } NATIVE_OS_RULE_MAP = { "darwin": "osx", "macos": "osx", "osx": "osx", "linux": "linux", "windows": "windows", } NATIVE_ARCH_KEY_MAP = { "x86": "x86", "i386": "x86", "i486": "x86", "i586": "x86", "i686": "x86", "x86_64": "x86_64", "amd64": "x86_64", "aarch64": "arm64", "arm64": "arm64", "aarch_64": "arm64", } NATIVE_OLD_TO_NEW_ARCH = { "32": "x86", "64": "x86_64", "x86": "x86", "x86_64": "x86_64", "amd64": "x86_64", "aarch64": "arm64", "arm64": "arm64", } NATIVE_NEW_TO_OLD_ARCH = { "x86": "32", "x86_64": "64", } logger = logging.getLogger("Launcher.CoreLib") def get_version_manifest(manifest_url=MOJANG_VERSION_MANIFEST_V2, timeout=10) -> dict | None: """ Get version manifest. :param manifest_url: Source of the version manifest. (Default use MOJANG_VERSION_MANIFEST_V2) :param timeout: Request timeout :return: version_manifest: dict """ logger.debug("Getting version manifest from {}".format(manifest_url)) try: r = requests.get(manifest_url, timeout=timeout) r.raise_for_status() except requests.exceptions.RequestException as e: raise VersionManifestFetchException( "Error getting version manifest from {}: {}".format(manifest_url, e) ) try: data = r.json() logger.debug("VersionManifest: {}".format(json.dumps(data, indent=4))) return data except ValueError as e: raise VersionManifestFetchException( "Unable to deserialize response data into dictionary: {}".format(e) ) def get_latest_version(version_manifest: dict, is_snapshot=False) -> None | str: """ Get latest version (id) from version manifest. :param version_manifest: Version manifest. :param is_snapshot: Return latest snapshot version. :return: latest_version: str """ latest = version_manifest.get("latest", {}) if len(latest) == 0: raise NoVersionFoundException( "No latest version section available in version manifest." ) latest_ver = latest.get("release" if not is_snapshot else "snapshot", None) if latest_ver is None: raise NoSpecifiedVersionKeyException( "No latest version available." ) else: logger.debug("Latest version: {}".format(latest_ver)) return latest_ver def get_all_versions(version_manifest: dict, is_snapshot=False, include_all_type=False, specified_type=None) -> list: """ Get all versions (id) from version manifest. :param version_manifest: :param is_snapshot: Return all versions that type is snapshot. :param include_all_type: Return all versions (all types included) :param specified_type: Specific version type :return: filtered_versions: list """ all_vers = version_manifest.get("versions", []) if len(all_vers) == 0: raise NoVersionFoundException( "No version section available in version manifest." ) if not specified_type: target_type = "release" if not is_snapshot else "snapshot" else: target_type = specified_type filtered = [] for ver in all_vers: ver_id = ver.get("id", None) ver_type = ver.get("type") logger.debug("Ver: {}, Type: {}".format(ver_id, ver_type)) if ver_type == target_type or include_all_type: filtered.append(ver) if len(filtered) == 0: raise NoSpecifiedVersionKeyException("No version matches specified type: {}".format(target_type)) return filtered def get_available_version_types(version_manifest: dict) -> list: """ Get available version types from version manifest. :param version_manifest: :return: available_types: list """ all_vers = version_manifest.get("versions", []) if len(all_vers) == 0: raise NoVersionFoundException( "No version section available in version manifest." ) types = [] for ver in all_vers: ver_type = ver.get("type") if ver_type not in types: types.append(ver_type) return types def get_specific_version_manifest_url(version_manifest: dict, version_id: str) -> str | None: """ Get specific version's manifest url from version manifest. :param version_manifest: :param version_id: :return: specific_version_manifest_url: str """ versions = version_manifest.get("versions", []) if len(versions) == 0: raise NoVersionFoundException( "No version section available in version manifest." ) for version in versions: if version.get("id") == version_id: specific_ver_manifest_url = version.get("url", None) if specific_ver_manifest_url is None: raise VersionManifestFetchException( "Specific version manifest URL not available in version manifest." ) return specific_ver_manifest_url raise NoSpecifiedVersionKeyException( "Specified version {} manifest URL not available in version manifest.".format(version_id) ) def get_specific_version_manifest_url_and_hash(version_manifest: dict, version_id: str) \ -> tuple[str,str] | tuple[None, None]: """ Get specific version's manifest url and hash from version manifest. :param version_manifest: :param version_id: :return: """ versions = version_manifest.get("versions", []) if len(versions) == 0: raise NoVersionFoundException( "No version section available in version manifest." ) for version in versions: if version.get("id") == version_id: specific_ver_manifest_url = version.get("url", None) specific_ver_manifest_hash = version.get("hash", None) if specific_ver_manifest_url is None: raise VersionManifestFetchException( "Specific version manifest URL is None. Is the manifest URL or source correct?" ) if specific_ver_manifest_hash is None: logger.debug("Specific version manifest hash not available in version manifest.") return specific_ver_manifest_url, specific_ver_manifest_hash raise NoSpecifiedVersionKeyException( "Specified version {} manifest URL not available in version manifest.".format(version_id) ) def fetch_specific_version_manifest(version_manifest: dict, spec_version: str) -> tuple[dict, str | None]: """ Get specific version's manifest data and hash from version manifest. :param version_manifest: :param spec_version: :return: specific_version_manifest_data: str hash: str (sha1) """ ver_url, sha1 = get_specific_version_manifest_url_and_hash(version_manifest, spec_version) try: r = requests.get(ver_url, timeout=10) r.raise_for_status() except requests.exceptions.RequestException as e: raise VersionManifestFetchException( "Failed to fetch version manifest URL: {}\n{}".format(ver_url, e) ) try: data = r.json() logger.debug("SpecificVersionManifest: {}".format(json.dumps(data, indent=4))) return data, sha1 except ValueError as e: raise VersionManifestFetchException( "Unable to deserialize response data into dictionary: {} (TargetVer: {})".format(e, ver_url) ) # mapping get_specific_version_manifest = fetch_specific_version_manifest def find_useful_part_in_specific_version_manifest(spec_version_manifest: dict)\ -> tuple[dict | None, dict | None, dict | None, dict | None, list | None, str | None, str | None]: """ Return useful part from version manifest. :param spec_version_manifest: Specific version's data :return: arguments: dict (contains jvm and game args, for newer version of Minecraft) asset_index: dict downloads: dict (Game core files are store in here) java_version: dict libraries: dict main_class: str legacy_game_args: str (aka "minecraftArguments" in old Minecraft version data) """ arguments = spec_version_manifest.get("arguments", {}) asset_index = spec_version_manifest.get("assetIndex", {}) downloads = spec_version_manifest.get("downloads", {}) java_version = spec_version_manifest.get("javaVersion", {}) libraries = spec_version_manifest.get("libraries", []) main_class = spec_version_manifest.get("mainClass", None) legacy_game_args = spec_version_manifest.get("minecraftArguments", None) if not arguments and not legacy_game_args: raise VersionDataKeyNotFoundException( "No version manifest available in version manifest. (\"arguments\" or \"minecraftArguments\")" ) if len(asset_index) == 0: raise VersionDataKeyNotFoundException( "No asset index section available in version manifest. (\"assetIndex\")" ) if len(downloads) == 0: raise VersionDataKeyNotFoundException( "No downloads section available in version manifest. (\"downloads\")" ) if len(java_version) == 0: raise VersionDataKeyNotFoundException( "No java version section available in version manifest. (\"javaVersion\")" ) if len(libraries) == 0: raise VersionDataKeyNotFoundException( "No libraries section available in version manifest. (\"libraries\")" ) if main_class is None: raise VersionDataKeyNotFoundException( "No main class section available in version manifest. (\"mainClass\")" ) return arguments, asset_index, downloads, java_version, libraries, main_class, legacy_game_args def get_core_file_download_url_and_hash(version_manifest: dict, is_server: bool=False) -> tuple[str, str] | tuple[None, None]: """ Get core file download url and hash from version manifest. :param version_manifest: :param is_server: Download server version core file (aka "server.jar") if is enabled. Default is False and ony download client version core file ("client.jar") :return: core_file_download_url: str hash: str (sha1) """ core_downloads = version_manifest.get("downloads", {}) if len(core_downloads) == 0: raise VersionDataKeyNotFoundException( "No downloads section available in version manifest. (\"downloads\")" ) core_name = "client" if not is_server else "server" core_section = core_downloads.get(core_name, {}) if len(core_section) == 0: raise VersionDataKeyNotFoundException( logger.debug("No target core section available in version manifest. Wants: {}".format(core_name)) ) download_url = core_section.get("url", None) sha1 = core_section.get("sha1", None) if download_url is None: raise VersionDataKeyNotFoundException( "Core file url is None.\n" ) if sha1 is None: warnings.warn( "Core file sha1 is None. This may cause security issues.\n" ) return download_url, sha1 def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_manifest=None) -> dict | None: """ Fetch and save version manifest. (Two types. One is root version manifest, another is specific version manifest) :param dest: Destination of the version manifest. :param target_version: Target version to fetch manifest from. Set this to a Minecraft version if you want to save its version data. :param root_manifest: Root manifest to fetch specific version manifest from. If not set the function will auto fetch it. :return: version_manifest: dict """ if root_manifest is None: root_manifest = get_version_manifest() if target_version: # For specified ver data, sha1 = fetch_specific_version_manifest(root_manifest, target_version) else: # For root version manifest (contains all versions) data = root_manifest sha1 = None file = FileObject( path=dest, sha1=sha1, ) dest.parent.mkdir(parents=True, exist_ok=True) try: with dest.open("w") as f: json.dump(data, f, indent=4) if sha1 is not None and not file.verify_sha1(sha1): raise HashMismatchException( "sha1", file.sha1, sha1, file.posix_path ) return data except Exception as e: raise VersionManifestSaveException( "Error saving version manifest:\n{}".format(e) )