Skip to content

Instantly share code, notes, and snippets.

@edomora97
Last active August 27, 2023 19:26
Show Gist options
  • Save edomora97/c41b8864f3280554b3aa058580534731 to your computer and use it in GitHub Desktop.
Save edomora97/c41b8864f3280554b3aa058580534731 to your computer and use it in GitHub Desktop.
Minecraft Launcher

Minecraft Offline Client Launcher

This script downloads from Mojang servers and runs the specified Minecraft version. It supports only offline (i.e. not online-verified) mode.

Usage

You have to have java, python3 and the python request library installed.

Run it like ./client_launcher.py 1.15.2 username

usage: client_launcher.py [-h] [--server SERVER] [--port PORT] [--java JAVA] [--silent] version username

Minecraft launcher

positional arguments:
  version          minecraft version
  username         offline username to use

optional arguments:
  -h, --help       show this help message and exit
  --server SERVER  hostname to connect to
  --port PORT      port of the server
  --java JAVA      path to the java executable
  --silent         disable the sounds in the client
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment