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