Initial commit
Dieser Commit ist enthalten in:
Commit
525fde83be
1
.gitignore
vendored
Normale Datei
1
.gitignore
vendored
Normale Datei
@ -0,0 +1 @@
|
|||||||
|
/.idea
|
318
launcher.py
Ausführbare Datei
318
launcher.py
Ausführbare Datei
@ -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()
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren