Last active
July 6, 2023 18:55
-
-
Save darkerbit/8b62dd4b8ed3f6fcac9f90e839e06e27 to your computer and use it in GitHub Desktop.
World's smallest Minecraft launcher, probably useful as an educational resource
This file contains 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
# TinyCraft: incredibly tiny Minecraft launcher | |
# Windows only, at present. | |
# Uses Mojang's bundled JVMs, downloading them as needed. | |
# Copyright (c) 2023 darkerbit | |
# | |
# This software is provided 'as-is', without any express or implied | |
# warranty. In no event will the authors be held liable for any damages | |
# arising from the use of this software. | |
# | |
# Permission is granted to anyone to use this software for any purpose, | |
# including commercial applications, and to alter it and redistribute it | |
# freely, subject to the following restrictions: | |
# | |
# 1. The origin of this software must not be misrepresented; you must not | |
# claim that you wrote the original software. If you use this software | |
# in a product, an acknowledgment in the product documentation would be | |
# appreciated but is not required. | |
# 2. Altered source versions must be plainly marked as such, and must not be | |
# misrepresented as being the original software. | |
# 3. This notice may not be removed or altered from any source distribution. | |
import json | |
import os | |
import shutil | |
import subprocess | |
import requests | |
from multiprocessing import Pool | |
from zipfile import ZipFile | |
# Settings | |
version = '1.20.1' | |
username = 'darkerbit' | |
def _download(url, dest): | |
if not os.path.exists(dest): | |
os.makedirs(os.path.dirname(dest), exist_ok=True) | |
with open(dest, 'wb') as f: | |
for chunk in requests.get(url, stream=True).iter_content(chunk_size=1000000): | |
f.write(chunk) | |
return dest | |
def download(url, dest): | |
res = _download(url, dest) | |
if res is not None: | |
print(res) | |
def idownload(item): | |
return _download(*item) | |
def download_queue(queue): | |
with Pool() as p: | |
for res in p.imap(idownload, queue): | |
if res is not None: | |
print(res) | |
def find_version(): | |
manifest = requests.get('https://piston-meta.mojang.com/mc/game/version_manifest_v2.json').json() | |
for v in manifest['versions']: | |
if v['id'] == version: | |
return v['url'] | |
raise Exception(f'No Minecraft version {version}') | |
def get_vjson(): | |
path = f'minecraft/versions/{version}/{version}.json' | |
if not os.path.exists(path): | |
download(find_version(), path) | |
with open(path, 'r') as f: | |
return json.load(f) | |
def get_asset_index(vjson): | |
path = f'minecraft/assets/indexes/{vjson["assetIndex"]["id"]}.json' | |
download(vjson['assetIndex']['url'], path) | |
with open(path, 'r') as f: | |
return json.load(f) | |
def find_jvm(component): | |
manifest = requests.get('https://launchermeta.mojang.com/v1/products/java-runtime' | |
'/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json').json() | |
return manifest['windows-x64'][component][0]['manifest']['url'] | |
def get_jvm_index(vjson): | |
if 'javaVersion' in vjson: | |
comp = vjson['javaVersion']['component'] | |
else: | |
# If a version doesn't specify a Java runtime, default to the legacy one most versions use | |
# Thank you, whoever wrote 1.6's JSON | |
comp = 'jre-legacy' | |
path = f'runtime/{comp}/{comp}.json' | |
if not os.path.exists(path): | |
download(find_jvm(comp), path) | |
with open(path, 'r') as f: | |
return json.load(f), comp | |
def queue_jar(queue, vjson): | |
path = f'minecraft/versions/{version}/{version}.jar' | |
queue.append((vjson['downloads']['client']['url'], path)) | |
return path | |
def queue_log4j2(queue, vjson): | |
if 'logging' in vjson: | |
path = f'minecraft/versions/{version}/log4j2.xml' | |
queue.append((vjson['logging']['client']['file']['url'], path)) | |
return vjson['logging']['client']['argument'].replace('${path}', path) | |
else: | |
return '' | |
def queue_assets(queue, index): | |
for obj in index['objects'].values(): | |
h = obj['hash'] | |
queue.append((f'https://resources.download.minecraft.net/{h[:2]}/{h}', f'minecraft/assets/objects/{h[:2]}/{h}')) | |
def queue_libraries(queue, vjson): | |
classpath = [] | |
natives = [] | |
# to prevent duplicates | |
processed = set() | |
for lib in vjson['libraries']: | |
if 'rules' in lib and 'os' in lib['rules'][0] and lib['rules'][0]['os']['name'] != 'windows': | |
continue | |
if 'natives' in lib: | |
classifier = lib['downloads']['classifiers'][lib['natives']['windows'].replace('${arch}', '64')] | |
sha1 = classifier['sha1'] | |
url = classifier['url'] | |
path = f'minecraft/libraries/{classifier["path"]}' | |
natives.append(path) | |
else: | |
sha1 = lib['downloads']['artifact']['sha1'] | |
url = lib['downloads']['artifact']['url'] | |
path = f'minecraft/libraries/{lib["downloads"]["artifact"]["path"]}' | |
classpath.append(path) | |
if sha1 not in processed: | |
processed.add(sha1) | |
queue.append((url, path)) | |
return classpath, natives | |
def queue_jvm(queue, comp, vjson, jvm_index): | |
for path, file in jvm_index['files'].items(): | |
if file['type'] != 'file': | |
continue | |
queue.append((file['downloads']['raw']['url'], f'runtime/{comp}/{path}')) | |
def copy_assets(index, dest): | |
for path, obj in index['objects'].items(): | |
h = obj['hash'] | |
os.makedirs(os.path.dirname(dest + path), exist_ok=True) | |
shutil.copy(f'minecraft/assets/objects/{h[:2]}/{h}', dest + path) | |
def process_assets(vjson, index): | |
if 'map_to_resources' in index and index['map_to_resources']: | |
copy_assets(index, 'minecraft/resources/') | |
if 'virtual' in index and index['virtual']: | |
copy_assets(index, f'minecraft/assets/virtual/{vjson["assetIndex"]["id"]}/') | |
def process_natives(natives): | |
for path in natives: | |
with ZipFile(path, 'r') as z: | |
z.extractall(f'minecraft/versions/{version}/natives') | |
def main(): | |
vjson = get_vjson() | |
index = get_asset_index(vjson) | |
jvm_index, comp = get_jvm_index(vjson) | |
queue = [] | |
jar = queue_jar(queue, vjson) | |
log_arg = queue_log4j2(queue, vjson) | |
queue_assets(queue, index) | |
classpath, natives = queue_libraries(queue, vjson) | |
queue_jvm(queue, comp, vjson, jvm_index) | |
download_queue(queue) | |
process_assets(vjson, index) | |
process_natives(natives) | |
classpath.append(jar) | |
if 'arguments' in vjson: | |
# This is a catastrophic mess. Too bad! | |
jvm = [] | |
for arg in vjson['arguments']['jvm']: | |
if type(arg) == str: | |
jvm.append(arg) | |
else: | |
if 'rules' in arg and \ | |
('os' not in arg['rules'][0] or | |
('name' in arg['rules'][0]['os'] and arg['rules'][0]['os']['name'] != 'windows')): | |
continue | |
jvm.append(arg['value']) | |
mc = [] | |
for arg in vjson['arguments']['game']: | |
if type(arg) == str: | |
mc.append(arg) | |
else: | |
if 'rules' in arg and \ | |
('os' not in arg['rules'][0] or | |
('name' in arg['rules'][0]['os'] and arg['rules'][0]['os']['name'] != 'windows')): | |
continue | |
mc.append(arg['value']) | |
jvm = ' '.join(jvm) | |
mc = ' '.join(mc) | |
else: | |
jvm = f'-Djava.library.path=minecraft/versions/{version}/natives ' \ | |
f'-Dminecraft.launcher.brand=tinycraft ' \ | |
f'-Dminecraft.launcher.version=1.0 ' \ | |
f'-cp {";".join(classpath)} ' \ | |
f'-Dos.name="Windows 10" -Dos.version=10.0 ' \ | |
f'-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump ' \ | |
f'-Xss1M' | |
mc = vjson['minecraftArguments'] | |
cmd = f'runtime\\{comp}\\bin\\java {jvm} {vjson["mainClass"]} {mc}' \ | |
.replace('${auth_player_name}', username) \ | |
.replace('${version_name}', version) \ | |
.replace('${game_directory}', 'minecraft') \ | |
.replace('${assets_root}', 'minecraft/assets') \ | |
.replace('${assets_index_name}', vjson['assetIndex']['id']) \ | |
.replace('${auth_uuid}', 'PLACEHOLDER') \ | |
.replace('${auth_access_token}', 'PLACEHOLDER') \ | |
.replace('${auth_session}', 'PLACEHOLDER') \ | |
.replace('${auth_xuid}', 'PLACEHOLDER') \ | |
.replace('${clientid}', 'PLACEHOLDER') \ | |
.replace('${user_type}', 'PLACEHOLDER') \ | |
.replace('${version_type}', vjson['type']) \ | |
.replace('${game_assets}', | |
f'minecraft/assets/virtual/{vjson["assetIndex"]["id"]}' if 'virtual' in index else 'minecraft/resources') \ | |
.replace('${natives_directory}', f'minecraft/versions/{version}/natives') \ | |
.replace('${launcher_name}', 'tinycraft') \ | |
.replace('${launcher_version}', '1.0') \ | |
.replace('${classpath}', ';'.join(classpath)) | |
print(f'Running {cmd}') | |
subprocess.run(cmd) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment