1
0
Dieser Commit ist enthalten in:
Lixfel 2022-04-13 22:16:25 +02:00
Commit 525fde83be
2 geänderte Dateien mit 319 neuen und 0 gelöschten Zeilen

1
.gitignore vendored Normale Datei
Datei anzeigen

@ -0,0 +1 @@
/.idea

318
launcher.py Ausführbare Datei
Datei anzeigen

@ -0,0 +1,318 @@
#!/bin/env python3
import sys
from typing import Optional
import requests
import json
import zipfile
import subprocess
from pathlib import Path
os_name = 'linux'
os_arch = 'x86'
base_path = Path.home() / Path('.minecraft')
jvm_params = ['-Xmx2G', '-Xshareclasses:nonfatal,name=minecraft', '-Xsyslog:none', '-Xtrace:none', '-Xdisableexplicitgc', '-XX:+AlwaysPreTouch', '-XX:+CompactStrings']
account_path = base_path / 'account.json'
versions_path = base_path / 'versions'
version_manifest_path = versions_path / 'version_manifest_v2.json'
assets_path = base_path / 'assets'
asset_indexes_path = assets_path / 'indexes'
asset_objects_path = assets_path / 'objects'
asset_log_path = assets_path / 'log_configs'
libraries_path = base_path / 'libraries'
native_base_path = base_path / 'bin'
def load_json(file: Path) -> dict:
with file.open('r') as f:
return json.load(f)
def download_to_file(url: str, file: Path):
print(f"Downloading {file.name}")
r = requests.get(url, stream=True)
file.parent.mkdir(parents=True, exist_ok=True)
with file.open('wb') as f:
for chunk in r.iter_content(chunk_size=4096):
if chunk:
f.write(chunk)
def get_version_manifest() -> dict:
if not version_manifest_path.exists():
download_to_file('https://launchermeta.mojang.com/mc/game/version_manifest.json', version_manifest_path)
return load_json(version_manifest_path)
def get_version_description(version: str, version_manifest: dict) -> dict:
version_description_path = versions_path / version / (version + '.json')
if not version_description_path.exists():
version_urls = [version_info['url'] for version_info in version_manifest['versions'] if version == version_info['id']]
if not version_urls:
version_manifest_path.unlink()
version_manifest = get_version_manifest()
version_urls = [version_info['url'] for version_info in version_manifest['versions'] if version == version_info['id']]
download_to_file(version_urls[0], version_description_path)
return load_json(version_description_path)
def load_assets(version_description: dict):
if 'assets' not in version_description:
return
asset_index = asset_indexes_path / (version_description['assets'] + '.json')
if not asset_index.exists():
download_to_file(version_description['assetIndex']['url'], asset_index)
for asset in load_json(asset_index)['objects'].values():
hash = asset['hash']
asset_path = asset_objects_path / hash[:2] / hash
if not asset_path.exists():
download_to_file(f'https://resources.download.minecraft.net/{hash[:2]}/{hash}', asset_path)
def load_logging(version_description: dict) -> list[str]:
if 'logging' not in version_description:
return []
logging_path = asset_log_path / version_description['logging']['client']['file']['id']
if not logging_path.exists():
download_to_file(version_description['logging']['client']['file']['url'], logging_path)
return [version_description['logging']['client']['argument'].replace('${path}', str(logging_path.absolute()))]
def rules_applying(object: dict):
if 'rules' not in object:
return True
def rule_applying(rule):
if 'os' in rule:
if 'name' in rule['os']:
return rule['os']['name'] == os_name
elif 'arch' in rule['os']:
return rule['os']['arch'] == os_arch
else:
raise Exception(rule['os'])
elif 'features' in rule:
if 'is_demo_user' in rule['features']:
return not rule['features']['is_demo_user']
elif 'has_custom_resolution' in rule['features']:
return rule['features']['has_custom_resolution']
else:
raise Exception(rule['features'])
return True
allowed = False
for rule in object['rules']:
if rule_applying(rule):
action = rule['action']
if action == 'allow':
allowed = True
elif action == 'disallow':
return False
else:
raise Exception(action)
return allowed
def load_libraries(version_description: dict, native_libraries_path: Path) -> list[Path]:
libraries = []
if 'libraries' not in version_description:
return libraries
for library in version_description['libraries']:
if not rules_applying(library):
continue
if 'downloads' in library:
library_path = libraries_path / library['downloads']['artifact']['path']
else:
package, name, version = library['name'].split(':')
path = f'{package.replace(".", "/")}/{name}/{version}/{name}-{version}.jar'
library_path = libraries_path / path
libraries.append(library_path)
if not library_path.exists():
if 'downloads' in library:
download_to_file(library['downloads']['artifact']['url'], library_path)
else:
download_to_file(library["url"] + path, library_path)
if 'natives' in library and os_name in library['natives']:
native_id = library['natives'][os_name]
native_path = libraries_path / library['downloads']['classifiers'][native_id]['path']
if not native_path.exists():
download_to_file(library['downloads']['classifiers'][native_id]['url'], native_path)
zip = zipfile.ZipFile(str(native_path))
for info in zip.infolist():
if info.is_dir():
continue
extracted_path = native_libraries_path / info.filename
if not extracted_path.exists():
extracted_path.parent.mkdir(parents=True, exist_ok=True)
with extracted_path.open('wb') as f:
f.write(zip.read(info.filename))
return libraries
def backup_dict(key: str, primary_dict: dict, backup_dict: dict):
return primary_dict[key] if key in primary_dict else backup_dict[key]
def parse_args(arg, version_description: dict, inherited_description: dict, native_libraries_path: Path, classpath: str, account: dict) -> list[str]:
if type(arg) is str:
return [arg.replace(
'${natives_directory}', str(native_libraries_path.absolute())
).replace(
'${launcher_name}', 'lixfel-launcher'
).replace(
'${launcher_version}', '1.0'
).replace(
'${classpath}', classpath
).replace(
'${auth_player_name}', account['username']
).replace(
'${version_name}', backup_dict('id', version_description, inherited_description)
).replace(
'${game_directory}', str(base_path.absolute())
).replace(
'${assets_root}', str(assets_path.absolute())
).replace(
'${assets_index_name}', backup_dict('assets', version_description, inherited_description)
).replace(
'${auth_uuid}', account['uuid']
).replace(
'${auth_access_token}', account['accessToken']
).replace(
'${clientid}', account['clientToken']
).replace(
'${auth_xuid}', 'null'
).replace(
'${user_type}', 'mojang'
).replace(
'${version_type}', backup_dict('type', version_description, inherited_description)
).replace(
'${resolution_width}', str(1280)
).replace(
'${resolution_height}', str(720)
)]
elif type(arg) is list:
return [v for subargs in arg for v in parse_args(subargs, version_description, inherited_description, native_libraries_path, classpath, account)]
elif type(arg) is dict:
if not rules_applying(arg):
return []
return parse_args(arg['value'], version_description, inherited_description, native_libraries_path, classpath, account)
raise Exception(arg)
def yggdrasil_request(url: str, payload: dict) -> Optional[dict]:
r = requests.post(f'https://authserver.mojang.com/{url}', json=payload, headers={'Content-Type': 'application/json'})
if len(r.content) == 0 and r.status_code // 100 == 2:
return None
result = json.loads(r.content.decode('utf-8'))
if r.status_code // 100 != 2:
raise Exception(result)
return result
def new_account_file(email: str, password: str, authenticate: dict) -> dict:
account = {
'email': email,
'password': password,
'accessToken': authenticate['accessToken'],
'clientToken': authenticate['clientToken'],
'uuid': authenticate['selectedProfile']['id'],
'username': authenticate['selectedProfile']['name']
}
with account_path.open('w') as f:
json.dump(account, f)
return account
def authenticate_mojang() -> dict:
if account_path.exists():
account = load_json(account_path)
try:
yggdrasil_request('validate', {'accessToken': account['accessToken'], 'clientToken': account['clientToken']})
return account
except:
print('Validation insufficient, trying refresh')
email = account['email']
password = account['password']
try:
refresh = yggdrasil_request('refresh', {'accessToken': account['accessToken'], 'clientToken': account['clientToken']})
return new_account_file(email, password, refresh)
except:
print('Refresh insufficient, trying authentication')
else:
import tkinter.simpledialog
email = tkinter.simpledialog.askstring(title='E-Mail', prompt='Mojang-Account E-Mail')
password = tkinter.simpledialog.askstring(title='Password', prompt='Mojang-Account Password')
try:
authenticate = yggdrasil_request('authenticate', {'agent': {'name': 'Minecraft', 'version': 1}, 'username': email, 'password': password})
except Exception as e:
import tkinter.messagebox
tkinter.messagebox.showerror(title='Could not authenticate', message='Could not authenticate')
raise e
return new_account_file(email, password, authenticate)
def main():
version_manifest = get_version_manifest()
version = version_manifest['latest']['release'] if len(sys.argv) <= 1 else sys.argv[1]
account = authenticate_mojang()
version_description = get_version_description(version, version_manifest)
inherited_description = get_version_description(version_description['inheritsFrom'],
version_manifest) if 'inheritsFrom' in version_description else None
load_assets(version_description)
if inherited_description:
load_assets(inherited_description)
native_libraries_path = native_base_path / version
libraries = load_libraries(version_description, native_libraries_path)
if inherited_description:
libraries += load_libraries(inherited_description, native_libraries_path)
client_path = versions_path / version / (version + '.jar')
libraries.append(client_path)
if not client_path.exists():
download_to_file(version_description['downloads']['client']['url'], client_path)
classpath = ':'.join(str(library.absolute()) for library in libraries)
args = ['java']
if inherited_description:
args += parse_args(inherited_description['arguments']['jvm'], version_description, inherited_description, native_libraries_path, classpath, account)
if 'jvm' in version_description['arguments']:
args += parse_args(version_description['arguments']['jvm'], version_description, inherited_description, native_libraries_path, classpath, account)
args += load_logging(version_description)
if inherited_description:
args += load_logging(inherited_description)
args += jvm_params
args.append(version_description['mainClass'])
if 'game' in version_description['arguments']:
args += parse_args(version_description['arguments']['game'], version_description, inherited_description, native_libraries_path, classpath, account)
if inherited_description:
args += parse_args(inherited_description['arguments']['game'], version_description, inherited_description, native_libraries_path, classpath, account)
subprocess.Popen(args, cwd=str(base_path.absolute()), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if __name__ == '__main__':
main()