Skip to content

Instantly share code, notes, and snippets.

@WitherOrNot
Created March 22, 2023 10:03
Show Gist options
  • Save WitherOrNot/07463e049e324a7e3c4982a4ec430a0f to your computer and use it in GitHub Desktop.
Save WitherOrNot/07463e049e324a7e3c4982a4ec430a0f to your computer and use it in GitHub Desktop.
Offline minecraft downloader/launcher
#!/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