Created
March 22, 2023 10:03
-
-
Save WitherOrNot/07463e049e324a7e3c4982a4ec430a0f to your computer and use it in GitHub Desktop.
Offline minecraft downloader/launcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import requests | |
import platform | |
import sys | |
import os | |
import lzma | |
import shlex | |
import json | |
from datetime import datetime | |
from uuid import uuid3 | |
from binascii import hexlify | |
from argparse import ArgumentParser | |
from subprocess import run | |
from shutil import copyfile | |
from zipfile import ZipFile | |
PORTABLE = False | |
VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json" | |
RUNTIME_MANIFEST_URL = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json" | |
OS = platform.system() | |
ARCH = ["x86", "x64"][int(sys.maxsize > 2**32)] | |
BITNESS = {"x86": "32", "x64": "64"}[ARCH] | |
version_manifest = requests.get(VERSION_MANIFEST_URL).json() | |
runtime_manifest = requests.get(RUNTIME_MANIFEST_URL).json() | |
def path_join(*args): | |
if OS == "Windows": | |
return os.path.join(*args).replace("/", "\\") | |
return os.path.join(*args) | |
if OS == "Windows": | |
MC_PARENT = os.environ["APPDATA"] | |
MC_FOLDER = path_join(MC_PARENT, ".minecraft") | |
MC_OS = "windows" | |
elif OS == "Linux": | |
MC_PARENT = os.environ["HOME"] | |
MC_FOLDER = path_join(MC_PARENT, ".minecraft") | |
MC_OS = "linux" | |
elif OS == "Darwin": | |
MC_PARENT = path_join(os.environ["HOME"], "Library", "Application Support") | |
MC_FOLDER = path_join(MC_PARENT, "minecraft") | |
MC_OS = "osx" | |
def aria2dl(dl_list): | |
def aria_entry(url, path, checksum, out=None): | |
basename = os.path.basename(path) if out is None else out | |
directory = os.path.dirname(path).replace("\\", "/") | |
return f"{url}\n\tout={basename}\n\tdir={directory}\n\tchecksum=sha-1={checksum}\n" | |
ariadl = "" | |
for entry in dl_list: | |
ariadl += aria_entry(*entry) | |
with open("dl.tmp", "w") as f: | |
f.write(ariadl) | |
run(["aria2c", "--console-log-level=error", "--summary-interval=0", "--download-result=hide", "-c", "-x", "16", "-j", "16", "-i", "dl.tmp"]) | |
print() | |
os.remove("dl.tmp") | |
return ariadl | |
def extract_jar(jarfile, directory): | |
with ZipFile(jarfile) as zipf: | |
for member in filter(lambda name: "META-INF" not in name, zipf.namelist()): | |
zipf.extract(member, path=directory) | |
def username_to_uuid(username): | |
if not PORTABLE: | |
return hexlify(uuid3(type("", (), dict(bytes=b""))(), "OfflinePlayer:" + username).bytes).decode("utf-8") | |
else: | |
return "a" * 32 | |
fix_format = lambda s: s.replace("${", "{") | |
def download_runtime(runtime, runtime_platform=None, install_dir=None): | |
runtime_platform = { | |
"Windows": { | |
"x86": "windows-x86", | |
"x64": "windows-x64" | |
}, | |
"Linux": { | |
"x86": "linux-i386", | |
"x64": "linux" | |
}, | |
"Darwin": { | |
"x86": "mac-os", | |
"x64": "mac-os" | |
} | |
}[OS][ARCH] if runtime_platform is None else runtime_platform | |
install_dir = path_join(MC_PARENT, "minecraft_runtimes", runtime) if install_dir is None else install_dir | |
os.makedirs(install_dir, exist_ok=True) | |
rt_files_url = runtime_manifest[runtime_platform][runtime][0]["manifest"]["url"] | |
rt_files = requests.get(rt_files_url).json()["files"] | |
lzma_files = [] | |
print(f"Downloading runtime files for {runtime_platform}...") | |
dl_list = [] | |
for filename in rt_files: | |
file = rt_files[filename] | |
filepath = path_join(install_dir, filename) | |
if file["type"] == "directory": | |
os.makedirs(filepath, exist_ok=True) | |
elif file["type"] == "file": | |
if "lzma" not in file["downloads"]: | |
fileurl = file["downloads"]["raw"]["url"] | |
filehash = file["downloads"]["raw"]["sha1"] | |
basename = os.path.basename(filename) | |
else: | |
fileurl = file["downloads"]["lzma"]["url"] | |
filehash = file["downloads"]["lzma"]["sha1"] | |
basename = os.path.basename(filename) + ".lzma" | |
lzma_files.append(filepath + ".lzma") | |
dl_list.append((fileurl, filepath, filehash, basename)) | |
aria2dl(dl_list) | |
for i, file in enumerate(lzma_files): | |
print(f"Decompressing runtime files ({i+1}/{len(lzma_files)})... \r", end="") | |
with lzma.open(file ,"rb") as lf: | |
raw_path = "".join(file.split(".lzma")[:-1]) | |
with open(raw_path, "wb") as f: | |
f.write(lf.read()) | |
if OS in ["Linux", "Darwin"]: | |
os.chmod(raw_path, 0o755) | |
os.remove(file) | |
print() | |
return install_dir | |
def parse_rules(obj_list): | |
out_list = [] | |
for i, obj in enumerate(obj_list): | |
if type(obj) == str: | |
if obj != "${classpath}": | |
out_list.append(fix_format(obj)) | |
elif type(obj) == dict and "rules" in obj: | |
allowed = False | |
for rule in obj["rules"]: | |
if rule["action"] == "allow": | |
allowed = "features" not in rule | |
if "os" in rule: | |
allowed = allowed and (OS.lower() == rule["os"].get("name", MC_OS) and ARCH == rule["os"].get("arch", ARCH) and "version" not in rule["os"]) | |
elif rule["action"] == "disallow": | |
if "os" in rule: | |
allowed = allowed and (OS.lower() != rule["os"].get("name", "") or ARCH != rule["os"].get("arch", "")) | |
if allowed: | |
if "value" in obj: | |
if type(obj["value"]) == str: | |
out_list.append(fix_format(obj["value"])) | |
elif type(obj["value"]) == list: | |
out_list.extend(list(map(fix_format, obj["value"]))) | |
elif "downloads" in obj: | |
del obj["rules"] | |
out_list.append(obj) | |
elif type(obj) == dict: | |
out_list.append(obj) | |
return out_list | |
def download_minecraft(version, mc_dir=None, runtime_dir=None, runtime_platform=None): | |
mc_dir = MC_FOLDER if mc_dir is None else mc_dir | |
version_obj = list(filter(lambda obj: obj.get("id", "") == version, version_manifest["versions"]))[0] | |
legacy_assets = datetime.fromisoformat(version_obj["releaseTime"]) <= datetime.fromisoformat("2013-10-25T13:00:00+00:00") | |
version_data = requests.get(version_obj["url"]).json() | |
runtime_type = version_data.get("javaVersion", {}).get("component", "jre-legacy") | |
runtime_dir = path_join(MC_PARENT, "minecraft_runtimes", runtime_type) if runtime_dir is None else path_join(runtime_dir, runtime_type) | |
if not os.path.exists(runtime_dir): | |
runtime_dir = download_runtime(runtime_type, install_dir=runtime_dir, runtime_platform=runtime_platform) | |
java_bin = { | |
"Windows": "java.exe", | |
"Linux": "java", | |
"Darwin": "java" | |
}[OS] | |
args = [path_join(runtime_dir, "bin", java_bin), "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:G1NewSizePercent=20", "-XX:G1ReservePercent=20", "-XX:MaxGCPauseMillis=50", "-XX:G1HeapRegionSize=32M", "-XX:ConcGCThreads=1", "-XX:ParallelGCThreads=4", "-Xmx2048M", "-Dfile.encoding=UTF-8"] | |
args.extend(parse_rules(version_data.get("arguments", {}).get("jvm", ["-Djava.library.path=" + path_join(mc_dir, "versions", version, "natives")]))) | |
assets_url = version_data["assetIndex"]["url"] | |
assets_index = assets_url.split("/")[-1] | |
assets = requests.get(assets_url).json() | |
print(f"Downloading Minecraft {version}...") | |
dl_urls = [(assets_url, path_join(mc_dir, "assets", "indexes", assets_index), version_data["assetIndex"]["sha1"])] | |
for asset in assets["objects"]: | |
asset_hash = assets["objects"][asset]["hash"] | |
asset_code = asset_hash[:2] | |
dl_urls.append((f"https://resources.download.minecraft.net/{asset_code}/{asset_hash}", path_join(mc_dir, "assets", "objects", asset_code, asset_hash), asset_hash)) | |
if assets.get("virtual", False): | |
dl_urls.append((f"https://resources.download.minecraft.net/{asset_code}/{asset_hash}", path_join(mc_dir, "assets", "virtual", "legacy", asset), asset_hash)) | |
if assets.get("map_to_resources", False): | |
dl_urls.append((f"https://resources.download.minecraft.net/{asset_code}/{asset_hash}", path_join(mc_dir, "resources", asset), asset_hash)) | |
native_paths = [] | |
classpaths = [] | |
for lib in parse_rules(version_data["libraries"]): | |
if "artifact" in lib["downloads"]: | |
lib_path = path_join(mc_dir, "libraries", lib["downloads"]["artifact"]["path"]) | |
lib_url = lib["downloads"]["artifact"]["url"] | |
lib_hash = lib["downloads"]["artifact"]["sha1"] | |
if lib_path not in classpaths: | |
dl_urls.append((lib_url, lib_path, lib_hash)) | |
if "patchy" not in lib_path: | |
classpaths.append(lib_path) | |
if "classifiers" in lib["downloads"]: | |
native_type = fix_format(lib["natives"][MC_OS]).format(arch=BITNESS) | |
if native_type in lib["downloads"]["classifiers"]: | |
native = lib["downloads"]["classifiers"][native_type] | |
native_path = path_join(mc_dir, "libraries", native["path"]) | |
native_url = native["url"] | |
native_hash = native["sha1"] | |
dl_urls.append((native_url, native_path, native_hash)) | |
native_paths.append(native_path) | |
client = version_data["downloads"]["client"] | |
client_url = client["url"] | |
client_path = path_join(mc_dir, "versions", version, f"{version}.jar") | |
client_hash = client["sha1"] | |
dl_urls.append((client_url, client_path, client_hash)) | |
aria2dl(dl_urls) | |
copyfile(client_path, client_path + ".bak") | |
classpaths.append(client_path) | |
cp_sep = { | |
"Windows": ";", | |
"Linux": ":", | |
"Darwin": ":" | |
}[OS] | |
if args[-1] != "-cp": | |
args.append("-cp") | |
args.append(cp_sep.join(classpaths)) | |
args.append(version_data["mainClass"]) | |
args.extend(parse_rules(version_data.get("arguments", {}).get("game", version_data.get("minecraftArguments", "").split(" ")))) | |
with open(path_join(mc_dir, "versions", version, f"{version}.json"), "w") as f: | |
f.write(json.dumps(version_data)) | |
with open(path_join(mc_dir, "versions", version, "launch.json"), "w") as f: | |
f.write(json.dumps(args)) | |
natives_dir = path_join(mc_dir, "versions", version, "natives") | |
os.makedirs(natives_dir, exist_ok=True) | |
for native in native_paths: | |
extract_jar(native, natives_dir) | |
def launch_minecraft(version, username="Player", mc_dir=None, runtime_dir=None, runtime_platform=None, dry_run=False): | |
mc_dir = MC_FOLDER if mc_dir is None else mc_dir | |
if version in ["latest", "latest_release", "release"]: | |
version = version_manifest["latest"]["release"] | |
elif version in ["latest_snapshot", "snapshot"]: | |
version = version_manifest["latest"]["snapshot"] | |
launch_path = path_join(mc_dir, "versions", version, "launch.json") | |
if not os.path.exists(launch_path): | |
download_minecraft(version, mc_dir=mc_dir, runtime_dir=runtime_dir, runtime_platform=runtime_platform) | |
with open(path_join(mc_dir, "versions", version, f"{version}.json")) as f: | |
version_data = json.loads(f.read()) | |
asset_index = version_data["assetIndex"]["id"] | |
runtime_type = version_data.get("javaVersion", {}).get("component", "jre-legacy") | |
runtime_dir = path_join(MC_PARENT, "minecraft_runtimes", runtime_type) if runtime_dir is None else path_join(runtime_dir, runtime_type) | |
if not os.path.exists(runtime_dir): | |
runtime_dir = download_runtime(runtime_type, install_dir=runtime_dir, runtime_platform=runtime_platform) | |
user_uuid = username_to_uuid(username) | |
print(f"Launching Minecraft {version} with username {username}...") | |
options = { | |
"auth_player_name": username, | |
"version_name": version, | |
"game_directory": mc_dir, | |
"game_assets": path_join(mc_dir, "assets"), | |
"assets_root": path_join(mc_dir, "assets"), | |
"assets_index_name": asset_index, | |
"auth_uuid": user_uuid, | |
"auth_access_token": user_uuid, | |
"auth_session": user_uuid, | |
"user_type": "legacy", | |
"user_properties": "{}", | |
"version_type": "release", | |
"natives_directory": path_join(mc_dir, "versions", version, "natives"), | |
"launcher_name": "java-minecraft-launcher", | |
"launcher_version": "6.9.420", | |
"clientid": "0", | |
"auth_xuid": "0" | |
} | |
with open(launch_path, "r") as f: | |
launch_args = json.loads(f.read()) | |
launch = list(map(lambda arg: arg.format(**options), launch_args)) | |
if not dry_run: | |
run(launch) | |
return launch | |
if __name__ == "__main__": | |
parser = ArgumentParser() | |
parser.add_argument("version", help="version to launch") | |
parser.add_argument("--username", "-u", help="username for minecraft", default="Player") | |
parser.add_argument("--runtime-dir", "-r", help="java runtime directory", default=None) | |
parser.add_argument("--mc-dir", "-m", help="minecraft directory", default=None) | |
args = parser.parse_args() | |
if args.version == "list": | |
print("All available versions:") | |
for version in version_manifest["versions"][::-1]: | |
print(version["id"]) | |
else: | |
launch_minecraft(args.version, args.username, mc_dir=args.mc_dir, runtime_dir=args.runtime_dir) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment