Files
Launcher/core_lib/game/version_info.py
T

494 lines
15 KiB
Python
Raw Normal View History

import json
import logging
import re
import warnings
from collections.abc import Callable
from pathlib import Path
import requests
from ..common.common import FileObject, download_file
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",
}
RUNTIME_PLATFORM_MAP = {
"linux": "linux",
"darwin": "mac-os",
"macos": "mac-os",
"osx": "mac-os",
"windows": "windows",
"win32": "windows",
"cygwin": "windows",
"msys": "windows",
}
RUNTIME_ARCH_EMPTY = "$EMPTY"
RUNTIME_ARCH_MAP = {
"linux": {
"i386": "i386",
"i686": "i386",
"x86": "i386",
"amd64": RUNTIME_ARCH_EMPTY,
"x86_64": RUNTIME_ARCH_EMPTY,
},
"windows": {
"i386": "x86",
"i686": "x86",
"x86": "x86",
"amd64": "x64",
"x86_64": "x64",
"aarch64": "arm64",
"arm64": "arm64",
},
"mac-os": {
"amd64": RUNTIME_ARCH_EMPTY,
"x86_64": RUNTIME_ARCH_EMPTY,
"aarch64": "arm64",
"arm64": "arm64",
},
}
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("sha1", 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[bytes, str | None]:
"""
Get specific version's manifest data and hash from version manifest.
:param version_manifest:
:param spec_version:
:return:
specific_version_manifest_data: bytes
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)
)
return r.content, sha1
# 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:
method = "wb" if isinstance(data, bytes) else "w"
with dest.open(method) as f:
if isinstance(data, bytes):
f.write(data)
elif isinstance(data, dict):
json.dump(data, f)
if sha1 is not None and not file.verify_sha1(sha1):
raise HashMismatchException(
"sha1",
file.sha1,
sha1,
file.posix_path
)
if isinstance(data, bytes):
# Convert the data back to dict
return json.loads(data.decode("utf-8"))
else:
return data
except Exception as e:
logger.exception(e)
raise VersionManifestSaveException(
"Error saving version manifest:\n{}".format(e)
)
def download_core_file(manifest: dict, destination: Path, is_server=False, raise_for_null_hash=False,
progress_callback: Callable[str]=None) -> FileObject:
"""
Download core file from version manifest.
:param manifest:
:param destination:
:param is_server:
:param raise_for_null_hash:
:param progress_callback:
:return:
client_file: FileObject
"""
url, sha1 = get_core_file_download_url_and_hash(manifest, is_server=is_server)
destination.parent.mkdir(parents=True, exist_ok=True)
file = FileObject(destination, url=url)
if sha1 is None and raise_for_null_hash:
raise VersionDataKeyNotFoundException(
"Core file hash is missing. Enable \"raise_for_null_hash\" flag to ignore this exception.\n"
)
elif sha1 is None:
logger.warning("Client has no sha1.")
if file.exists and file.verify("sha1", sha1):
logger.info(f"Client existed and sha1 matches.")
elif url is not None:
download_file(file, progress_callback=progress_callback)
elif not url:
raise VersionDataKeyNotFoundException(
"Core file url is None. Unable to download it."
)
return file