|
#!/usr/bin/env python3 |
|
|
|
from pathlib import Path |
|
import json |
|
import requests |
|
import shlex |
|
import os |
|
import uuid |
|
import zipfile |
|
import shutil |
|
import platform |
|
from multiprocessing import Pool |
|
|
|
PLATFORM = platform.system().lower() |
|
ARCH = platform.machine() |
|
CLIENT_PATH = Path(__file__).absolute().parent / "client" |
|
VERSION_PATH = CLIENT_PATH / "versions" |
|
LIBS_PATH = CLIENT_PATH / "libraries" |
|
ASSETS_PATH = CLIENT_PATH / "assets" |
|
MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json" |
|
OBJECTS_URL = "http://resources.download.minecraft.net/" |
|
|
|
|
|
def get_store_dir(version): |
|
return VERSION_PATH / version |
|
|
|
|
|
def get_native_dir(version): |
|
return VERSION_PATH / version / "native" |
|
|
|
|
|
def download_file(url, path): |
|
with path.open("wb") as f: |
|
for chunk in requests.get(url).iter_content(chunk_size=1024): |
|
f.write(chunk) |
|
|
|
|
|
def download_object(obj): |
|
(name, asset) = obj |
|
object_id = Path(asset["hash"][:2]) / asset["hash"] |
|
path = ASSETS_PATH / "objects" / object_id |
|
if path.exists(): |
|
return name |
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
url = OBJECTS_URL + str(object_id) |
|
download_file(url, path) |
|
return name |
|
|
|
|
|
def ensure_assets(version, manifest): |
|
index_path = ASSETS_PATH / "indexes" / \ |
|
(manifest["assetIndex"]["id"] + ".json") |
|
if not index_path.exists(): |
|
url = manifest["assetIndex"]["url"] |
|
index = requests.get(url).json() |
|
index_path.parent.mkdir(parents=True, exist_ok=True) |
|
with index_path.open("w") as f: |
|
json.dump(index, f) |
|
else: |
|
with index_path.open() as f: |
|
index = json.load(f) |
|
objects = index["objects"].items() |
|
pool = Pool(5) |
|
for (num, name) in enumerate(pool.imap_unordered(download_object, objects)): |
|
print("Downloading asset [%d/%d] %s" % (num+1, len(objects), name)) |
|
|
|
|
|
def download_manifest(version): |
|
manifest = requests.get(MANIFEST_URL).json() |
|
for v in manifest["versions"]: |
|
if v["id"] == version: |
|
url = v["url"] |
|
return requests.get(url).json() |
|
raise RuntimeError("Version %s not found" % version) |
|
|
|
|
|
def download_lib(name, lib): |
|
path = LIBS_PATH / lib["path"] |
|
url = lib["url"] |
|
if path.exists(): |
|
return path |
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
print("Downloading lib %s" % name) |
|
download_file(url, path) |
|
return path |
|
|
|
|
|
def ensure_libs(version, manifest): |
|
class_paths = [] |
|
for lib in manifest["libraries"]: |
|
name = lib["name"] |
|
if "rules" in lib and not check_rules(lib["rules"]): |
|
continue |
|
if "artifact" in lib["downloads"]: |
|
class_paths += [download_lib(lib["name"], |
|
lib["downloads"]["artifact"])] |
|
if "natives" in lib and PLATFORM in lib["natives"]: |
|
platform_tag = lib["natives"][PLATFORM] |
|
if platform_tag not in lib["downloads"]["classifiers"]: |
|
continue |
|
native = lib["downloads"]["classifiers"][platform_tag] |
|
path = get_native_dir(version) / native["path"] |
|
if path.exists(): |
|
continue |
|
url = native["url"] |
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
print("Download native lib %s" % name) |
|
download_file(url, path) |
|
print("Extracting native lib %s" % name) |
|
with zipfile.ZipFile(path) as z: |
|
z.extractall(get_native_dir(version)) |
|
return class_paths |
|
|
|
|
|
def ensure_main(version, manifest): |
|
path = VERSION_PATH / version / (version + ".jar") |
|
if path.exists(): |
|
return path |
|
url = manifest["downloads"]["client"]["url"] |
|
print("Downloading main jar") |
|
download_file(url, path) |
|
return path |
|
|
|
|
|
def disable_sound(version): |
|
path = VERSION_PATH / version / "options.txt" |
|
if path.exists(): |
|
with path.open() as f: |
|
options = f.read() |
|
import re |
|
options = re.sub("soundCategory_master:.+", |
|
"soundCategory_master:0.0", options) |
|
with path.open("w") as f: |
|
f.write(options) |
|
else: |
|
with path.open("w") as f: |
|
f.write("soundCategory_master:0.0\n") |
|
|
|
|
|
def check_rules(rules): |
|
for rule in rules: |
|
if rule["action"] == "allow": |
|
if "os" in rule: |
|
os = rule["os"] |
|
if "name" in os and os["name"] == PLATFORM: |
|
return True |
|
if "arch" in os and os["arch"] == ARCH: |
|
return True |
|
else: |
|
return True |
|
return False |
|
|
|
|
|
def get_raw_arguments(manifest): |
|
cmd = [] |
|
if "arguments" in manifest: |
|
for arg in manifest["arguments"]["jvm"]: |
|
if isinstance(arg, str): |
|
cmd += [arg] |
|
elif "rules" in arg: |
|
if check_rules(arg["rules"]): |
|
if isinstance(arg["value"], str): |
|
cmd += [arg["value"]] |
|
else: |
|
cmd += arg["value"] |
|
cmd += [manifest["mainClass"]] |
|
for arg in manifest["arguments"]["game"]: |
|
if isinstance(arg, str): |
|
cmd += [arg] |
|
elif "minecraftArguments": |
|
args = shlex.split(manifest["minecraftArguments"]) |
|
cmd += ["-Djava.library.path=${natives_directory}"] |
|
cmd += ["-Dminecraft.launcher.brand=${launcher_name}"] |
|
cmd += ["-Dminecraft.launcher.version=${launcher_version}"] |
|
cmd += ["-cp"] |
|
cmd += ["${classpath}"] |
|
cmd += [manifest["mainClass"]] |
|
for arg in args: |
|
cmd += [arg] |
|
else: |
|
raise RuntimeError("Arguments not present") |
|
return cmd |
|
|
|
|
|
def get_arguments(manifest, vars): |
|
cmd = [] |
|
for arg in get_raw_arguments(manifest): |
|
cmd += [arg.replace("$", "").format(**vars)] |
|
return cmd |
|
|
|
|
|
def ensure_client(version, java, username): |
|
manifest_path = get_store_dir(version) / (version + ".json") |
|
if not manifest_path.exists(): |
|
manifest = download_manifest(version) |
|
manifest_path.parent.mkdir(parents=True, exist_ok=True) |
|
with manifest_path.open("w") as f: |
|
json.dump(manifest, f) |
|
else: |
|
with manifest_path.open() as f: |
|
manifest = json.load(f) |
|
ensure_assets(version, manifest) |
|
class_paths = ensure_libs(version, manifest) |
|
class_paths += [ensure_main(version, manifest)] |
|
game_dir = VERSION_PATH / version |
|
vars = { |
|
"auth_player_name": username, |
|
"auth_uuid": str(uuid.uuid4()), |
|
"auth_access_token": "deadbeef", |
|
"game_directory": str(game_dir), |
|
"assets_root": str(ASSETS_PATH), |
|
"assets_index_name": manifest["assetIndex"]["id"], |
|
"natives_directory": str(get_native_dir(version)), |
|
"user_type": "mojang", |
|
"user_properties": "{}", |
|
"version_type": manifest["type"], |
|
"launcher_name": "java-minecraft-launcher", |
|
"launcher_version": "minecraft-stat", |
|
"classpath": ":".join(map(str, class_paths)), |
|
"version_name": version |
|
} |
|
cmd = [shutil.which(java)] |
|
cmd += get_arguments(manifest, vars) |
|
return cmd |
|
|
|
|
|
def main(): |
|
import argparse |
|
parser = argparse.ArgumentParser(description="Minecraft launcher") |
|
parser.add_argument("version", help="minecraft version") |
|
parser.add_argument("username", help="offline username to use") |
|
parser.add_argument("--server", help="hostname to connect to") |
|
parser.add_argument("--port", help="port of the server", |
|
type=int, default=25565) |
|
parser.add_argument("--java", help="path to the java executable", |
|
default="java") |
|
parser.add_argument("--silent", help="disable the sounds in the client", |
|
action="store_true") |
|
args = parser.parse_args() |
|
cmd = ensure_client(args.version, args.java, args.username) |
|
if args.server: |
|
cmd += ["--server", args.server, "--port", str(args.port)] |
|
if args.silent: |
|
disable_sound(args.version) |
|
os.chdir(VERSION_PATH / args.version) |
|
os.execv(cmd[0], cmd) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |