Skip to content

Instantly share code, notes, and snippets.

@darkerbit
Last active July 6, 2023 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darkerbit/8b62dd4b8ed3f6fcac9f90e839e06e27 to your computer and use it in GitHub Desktop.
Save darkerbit/8b62dd4b8ed3f6fcac9f90e839e06e27 to your computer and use it in GitHub Desktop.
World's smallest Minecraft launcher, probably useful as an educational resource
# 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