From 97577de5d01c359cb804f323f63db25c539b8ea3 Mon Sep 17 00:00:00 2001 From: Nel-S <75831544+Nel-S@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:41:18 -0700 Subject: [PATCH 1/3] Allow program to be run from outside its own directory + other updates - Add the path used to open the program from to all relative paths, so program doesn't create new folders in a directory outside the one it's located within - Replace sys.exits with exception raising (see https://github.com/hube12/DecompilerMC/pull/55) - Replace assertion (which can be compiled out) with unskippable test + exception - Tweak url['..._mappings'] code so correct exception is raised (https://github.com/hube12/DecompilerMC/issues/50) - Standardize pathlib usage - Some code restructuring to reduce the amount of indentation - Add some typing - Grammar fixes in code outputs/errors - Add TODOs to mark areas for future improvement --- main.py | 610 +++++++++++++++++++++++++++----------------------------- 1 file changed, 289 insertions(+), 321 deletions(-) diff --git a/main.py b/main.py index 5a0d41d..4d78068 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse import glob +import hashlib import json import os import random @@ -14,46 +15,45 @@ from pathlib import Path from shutil import which from subprocess import CalledProcessError -from typing import Union +from typing import Literal, TypeAlias, Union from urllib.error import HTTPError, URLError -assert sys.version_info >= (3, 7) +if sys.version_info < (3, 7): raise OSError("Python verson must be 3.7 or above.") CFR_VERSION = "0.152" SPECIAL_SOURCE_VERSION = "1.11.4" MANIFEST_LOCATION = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" CLIENT = "client" SERVER = "server" +SideType: TypeAlias = Literal['client', 'server'] +PATH_TO_ROOT_DIR = Path(os.path.dirname(sys.argv[0])) -def get_minecraft_path(): +def get_minecraft_path() -> Path: if sys.platform.startswith('linux'): - return Path("~/.minecraft") + return Path("~", ".minecraft") elif sys.platform.startswith('win'): - return Path("~/AppData/Roaming/.minecraft") + return Path("~", "AppData", "Roaming", ".minecraft") elif sys.platform.startswith('darwin'): - return Path("~/Library/Application Support/minecraft") - else: - print("Cannot detect of version : %s. Please report to your closest sysadmin" % sys.platform) - sys.exit(-1) + return Path("~", "Library", "Application Support", "minecraft") + raise RuntimeError(f"Platform {sys.platform} is not supported.") mc_path = get_minecraft_path() -def str2bool(v): +def str2bool(v: str | bool) -> bool: if isinstance(v, bool): return v if v.lower() in ('yes', 'true', 't', 'y', '1'): return True elif v.lower() in ('no', 'false', 'f', 'n', '0'): return False - else: - raise argparse.ArgumentTypeError('Boolean value expected.') + raise argparse.ArgumentTypeError(f'Could not convert {v} to a Boolean value.') -def check_java(): - """Check for java and setup the proper directory if needed""" +def check_java() -> None: + """Check for Java and setup the proper directory if needed.""" results = [] if sys.platform.startswith('win'): if not results: @@ -103,21 +103,19 @@ def check_java(): results.append(which('java', path='/opt')) results = [path for path in results if path is not None] if not results: - print('Java JDK is not installed ! Please install java JDK from https://java.oracle.com or OpenJDK') - input("Aborting, press anything to exit") - sys.exit(1) + raise RuntimeError('Java JDK is not installed! Please install a Java JDK from https://java.oracle.com, or install OpenJDK.') -def get_global_manifest(quiet): - if Path(f"versions/version_manifest.json").exists() and Path(f"versions/version_manifest.json").is_file(): +def get_global_manifest(quiet) -> None: + versionManifsetPath = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") + if versionManifsetPath.is_file(): if not quiet: - print( - "Manifest already existing, not downloading again, if you want to please accept safe removal at beginning") + print(f"Manifest already exists; not downloading again. If another manifest is wanted, please delete manually before running the program (location: {versionManifsetPath}).") return - download_file(MANIFEST_LOCATION, f"versions/version_manifest.json", quiet) + download_file(MANIFEST_LOCATION, versionManifsetPath, quiet) -def download_file(url, filename, quiet): +def download_file(url, filename: Path, quiet) -> None: try: if not quiet: print(f'Downloading {filename}...') @@ -125,57 +123,51 @@ def download_file(url, filename, quiet): with open(filename, 'wb+') as local_file: local_file.write(f.read()) except HTTPError as e: - if not quiet: - print('HTTP Error') - print(e) - sys.exit(-1) + raise RuntimeError(f'HTTP Error: {e}') except URLError as e: - if not quiet: - print('URL Error') - print(e) - sys.exit(-1) + raise RuntimeError(f'URL Error: {e}') -def get_latest_version(): - download_file(MANIFEST_LOCATION, f"manifest.json", True) - path_to_json = Path(f'manifest.json') +def get_latest_version() -> tuple[str, str]: + path_to_json = (PATH_TO_ROOT_DIR / 'manifest.json') + download_file(MANIFEST_LOCATION, path_to_json, True) snapshot = None - version = None - if path_to_json.exists() and path_to_json.is_file(): + release = None + if path_to_json.is_file(): path_to_json = path_to_json.resolve() with open(path_to_json) as f: versions = json.load(f)["latest"] - if versions and versions.get("release") and versions.get("release"): - version = versions.get("release") - snapshot = versions.get("snapshot") + if versions and versions.get("release"): + release: str = versions.get("release") + if versions and versions.get("snapshot"): + snapshot: str = versions.get("snapshot") path_to_json.unlink() - return snapshot, version + if release is None: + raise RuntimeError("Could not get latest release. Please refresh cache.") + if snapshot is None: + raise RuntimeError("Could not get latest snapshot. Please refresh cache.") + return snapshot, release -def get_version_manifest(target_version, quiet): - if Path(f"versions/{target_version}/version.json").exists() and Path(f"versions/{target_version}/version.json").is_file(): +def get_version_manifest(target_version: str, quiet) -> None: + versionPath = (PATH_TO_ROOT_DIR / "versions" / target_version / "version.json") + if versionPath.is_file(): if not quiet: - print( - "Version manifest already existing, not downloading again, if you want to please accept safe removal at beginning") + print(f"Version manifest already exists; not downloading again. If another version manifest is wanted, please delete manually before running the program (location: {versionPath}).") return - path_to_json = Path('versions/version_manifest.json') - if path_to_json.exists() and path_to_json.is_file(): - path_to_json = path_to_json.resolve() - with open(path_to_json) as f: - versions = json.load(f)["versions"] - for version in versions: - if version.get("id") and version.get("id") == target_version and version.get("url"): - download_file(version.get("url"), f"versions/{target_version}/version.json", quiet) - break - else: - if not quiet: - print('ERROR: Missing manifest file: version.json') - input("Aborting, press anything to exit") - sys.exit(-1) - - -def sha256(fname: Union[Union[str, bytes], int]): - import hashlib + path_to_json = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") + if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError(f'Missing manifest file: {path_to_json}') + path_to_json = path_to_json.resolve() + with open(path_to_json) as f: + versions = json.load(f)["versions"] + for version in versions: + if version.get("id") and version.get("id") == target_version and version.get("url"): + download_file(version.get("url"), versionPath, quiet) + break + + + +def sha256(fname: Union[Union[str, bytes], int]) -> str: hash_sha256 = hashlib.sha256() with open(fname, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): @@ -183,249 +175,235 @@ def sha256(fname: Union[Union[str, bytes], int]): return hash_sha256.hexdigest() -def get_version_jar(target_version, side, quiet): - path_to_json = Path(f"versions/{target_version}/version.json") - if Path(f"versions/{target_version}/{side}.jar").exists() and Path(f"versions/{target_version}/{side}.jar").is_file(): +def get_version_jar(target_version: str, side: SideType, quiet) -> None: + path_to_json = (PATH_TO_ROOT_DIR / "versions" / target_version / "version.json") + targetSidePath = (PATH_TO_ROOT_DIR / "versions" / target_version / f"{side}.jar") + if targetSidePath.is_file(): if not quiet: - print(f"versions/{target_version}/{side}.jar already existing, not downloading again") + print(f"Version jar already exists; not downloading again. If another version jar is wanted, please delete manually before running the program (location: {targetSidePath}).") return - if path_to_json.exists() and path_to_json.is_file(): - path_to_json = path_to_json.resolve() - with open(path_to_json) as f: - jsn = json.load(f) - if jsn.get("downloads") and jsn.get("downloads").get(side) and jsn.get("downloads").get(side).get("url"): - jar_path = f"versions/{target_version}/{side}.jar" - download_file(jsn.get("downloads").get(side).get("url"), jar_path, quiet) - # In case the server is newer than 21w39a you need to actually extract it first from the archive - if side == SERVER: - if Path(jar_path).exists(): - with zipfile.ZipFile(jar_path, mode="r") as z: - content = None - try: - content = z.read("META-INF/versions.list") - except Exception as _: - # we don't have a versions.list in it - pass - if content is not None: - element = content.split(b"\t") - if len(element) != 3: - print(f"Jar should be extracted but version list is not in the correct format, expected 3 fields, got {len(element)} for {content}") - sys.exit(-1) - version_hash = element[0].decode() - version = element[1].decode() - path = element[2].decode() - if version != target_version and not quiet: - print(f"Warning, version is not identical to the one targeted got {version} exepected {target_version}") - new_jar_path = f"versions/{target_version}" - try: - new_jar_path = z.extract(f"META-INF/versions/{path}", new_jar_path) - except Exception as e: - print(f"Could not extract to {new_jar_path} with error {e}") - sys.exit(-1) - if Path(new_jar_path).exists(): - file_hash = sha256(new_jar_path) - if file_hash != version_hash: - print(f"Extracted file hash and expected hash did not match up, got {file_hash} expected {version_hash}") - sys.exit(-1) - try: - shutil.move(new_jar_path, jar_path) - shutil.rmtree(f"versions/{target_version}/META-INF") - except Exception as e: - print("Exception while removing the temp file", e) - sys.exit(-1) - else: - print(f"New {side} jar could not be extracted from archive at {new_jar_path}, failure") - sys.exit(-1) - else: - print(f"Jar was maybe downloaded but not located, this is a failure, check path at {jar_path}") - sys.exit(-1) - else: - if not quiet: - print("Could not download jar, missing fields") - input("Aborting, press anything to exit") - sys.exit(-1) - else: - if not quiet: - print('ERROR: Missing manifest file: version.json') - input("Aborting, press anything to exit") - sys.exit(-1) + if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError(f'Missing manifest file: {path_to_json}') + path_to_json = path_to_json.resolve() + with open(path_to_json) as f: + jsn = json.load(f) + if not jsn.get("downloads") or not jsn.get("downloads").get(side) or not jsn.get("downloads").get(side).get("url"): + raise RuntimeError("Could not download jar, missing fields") + download_file(jsn.get("downloads").get(side).get("url"), targetSidePath, quiet) + # In case the server is newer than 21w39a you need to actually extract it first from the archive + if side == SERVER: + if not targetSidePath.exists(): + raise RuntimeError(f"Jar was maybe downloaded but not located, this is a failure, check path at {targetSidePath}") + with zipfile.ZipFile(targetSidePath, mode="r") as z: + content = None + try: + content = z.read(Path(f"META-INF", "versions.list")) + except Exception as _: + # we don't have a versions.list in it + pass + if content is not None: + element = content.split(b"\t") + if len(element) != 3: + raise RuntimeError(f"Jar should be extracted but version list is not in the correct format, expected 3 fields, got {len(element)} for {content}") + version_hash = element[0].decode() + version = element[1].decode() + path = element[2].decode() + if version != target_version and not quiet: + print(f"Warning: received version ({version}) does not match the targeted version ({target_version}).") + new_jar_path = (PATH_TO_ROOT_DIR / "versions" / target_version) + try: + new_jar_path = z.extract(Path("META-INF", "versions", path), new_jar_path) + except Exception as e: + raise RuntimeError(f"Could not extract to {new_jar_path}: {e}") + if not (PATH_TO_ROOT_DIR / new_jar_path).exists(): + raise RuntimeError(f"New {side} jar could not be extracted from archive at {new_jar_path}.") + file_hash = sha256(new_jar_path) + if file_hash != version_hash: + raise RuntimeError(f"Extracted file's hash ({file_hash}) and expected hash ({version_hash}) did not match.") + try: + shutil.move((PATH_TO_ROOT_DIR / new_jar_path), targetSidePath) + shutil.rmtree((PATH_TO_ROOT_DIR / "versions" / target_version / "META-INF")) + except Exception as e: + raise RuntimeError("Exception while removing the temp file", e) if not quiet: print("Done !") -def get_mappings(version, side, quiet): - if Path(f'mappings/{version}/{side}.txt').exists() and Path(f'mappings/{version}/{side}.txt').is_file(): +def get_mappings(version: str, side: SideType, quiet) -> None: + versionSidePath = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.txt") + if versionSidePath.is_file(): if not quiet: - print( - "Mappings already existing, not downloading again, if you want to please accept safe removal at beginning") + print(f"Mappings already exist; not downloading again. If other mappings are wanted, please delete manually before running the program (location: {versionSidePath}).") return - path_to_json = Path(f'versions/{version}/version.json') - if path_to_json.exists() and path_to_json.is_file(): - if not quiet: - print(f'Found {version}.json') - path_to_json = path_to_json.resolve() - with open(path_to_json) as f: - jfile = json.load(f) - url = jfile['downloads'] - if side == CLIENT: # client: - if url['client_mappings']: - url = url['client_mappings']['url'] - else: - if not quiet: - print(f'Error: Missing client mappings for {version}') - elif side == SERVER: # server - if url['server_mappings']: - url = url['server_mappings']['url'] - else: - if not quiet: - print(f'Error: Missing server mappings for {version}') - else: - if not quiet: - print('ERROR, type not recognized') - sys.exit(-1) - if not quiet: - print(f'Downloading the mappings for {version}...') - download_file(url, f'mappings/{version}/{"client" if side == CLIENT else "server"}.txt', quiet) - else: + path_to_json = (PATH_TO_ROOT_DIR / "versions" / version / "version.json") + if not path_to_json.exists() or not path_to_json.is_file(): + raise RuntimeError(f'Missing manifest file: {path_to_json}') + if not quiet: + print(f'Found {path_to_json}') + path_to_json = path_to_json.resolve() + with open(path_to_json) as f: + jfile = json.load(f) + url = jfile['downloads'] + if side == CLIENT: # client: + if 'client_mappings' not in url or 'url' not in url['client_mappings']: + #TODO: Clean up failed run before raising + raise RuntimeError(f'Could not find client mappings for {version}') + url = url['client_mappings']['url'] + elif side == SERVER: # server + if 'server_mappings' not in url or 'url' not in url['server_mappings']: + #TODO: Clean up failed run before raising + raise RuntimeError(f'Could not find server mappings for {version}') + url = url['server_mappings']['url'] + else: + raise RuntimeError('Type not recognized.') if not quiet: - print('ERROR: Missing manifest file: version.json') - input("Aborting, press anything to exit") - sys.exit(-1) + print(f'Downloading the mappings for {version}...') + download_file(url, (PATH_TO_ROOT_DIR / "mappings" / version / f"{'client' if side == CLIENT else 'server'}.txt"), quiet) -def remap(version, side, quiet): +def remap(version: str, side: SideType, quiet) -> None: if not quiet: print('=== Remapping jar using SpecialSource ====') t = time.time() - path = Path(f'versions/{version}/{side}.jar') + path = (PATH_TO_ROOT_DIR / "versions" / version / f"{side}.jar") # that part will not be assured by arguments if not path.exists() or not path.is_file(): - path_temp = (mc_path / f'versions/{version}/{version}.jar').expanduser() - if path_temp.exists() and path_temp.is_file(): - r = input("Error, defaulting to client.jar from your local Minecraft folder, continue? (y/n)") or "y" + path_temp = (mc_path / "versions" / version / f"{version}.jar").expanduser() + if path_temp.is_file(): + # TODO: Automate choice if auto mode is enabled + r = input("Error: defaulting to client.jar from your local Minecraft folder. Continue? (y/n)") or "y" if r != "y": + # TODO: Replace with something else sys.exit(-1) path = path_temp - mapp = Path(f'mappings/{version}/{side}.tsrg') - specialsource = Path(f'./lib/SpecialSource-{SPECIAL_SOURCE_VERSION}.jar') - if path.exists() and mapp.exists() and specialsource.exists() and path.is_file() and mapp.is_file() and specialsource.is_file(): - path = path.resolve() - mapp = mapp.resolve() - specialsource = specialsource.resolve() - subprocess.run(['java', - '-jar', specialsource.__str__(), - '--in-jar', path.__str__(), - '--out-jar', f'./src/{version}-{side}-temp.jar', - '--srg-in', mapp.__str__(), - "--kill-lvt" # kill snowmen - ], check=True, capture_output=quiet) - if not quiet: - print(f'- New -> {version}-{side}-temp.jar') - t = time.time() - t - print('Done in %.1fs' % t) - else: - if not quiet: - print( - f'ERROR: Missing files: ./lib/SpecialSource-{SPECIAL_SOURCE_VERSION}.jar or mappings/{version}/{side}.tsrg or versions/{version}/{side}.jar') - input("Aborting, press anything to exit") - sys.exit(-1) + if not path.exists() or not path.is_file(): + raise RuntimeError(f'Missing file: {path}') + path = path.resolve() + + mapp = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.tsrg") + if not mapp.exists() or not mapp.is_file(): + raise RuntimeError(f'Missing file: {mapp}') + mapp = mapp.resolve() + + specialsource = (PATH_TO_ROOT_DIR / "lib"/ f"SpecialSource-{SPECIAL_SOURCE_VERSION}.jar") + if not specialsource.exists() or not specialsource.is_file(): + raise RuntimeError(f'Missing file: {specialsource}') + specialsource = specialsource.resolve() + outJarPath = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + + subprocess.run(['java', + '-jar', specialsource.__str__(), + '--in-jar', path.__str__(), + '--out-jar', outJarPath, + '--srg-in', mapp.__str__(), + "--kill-lvt" # kill snowmen + ], check=True, capture_output=quiet) + if not quiet: + print(f'Created {outJarPath}.') + t = time.time() - t + print('Done in %.1fs' % t) -def decompile_fern_flower(decompiled_version, version, side, quiet, force): +def decompile_fern_flower(decompiled_version: str, version: str, side: SideType, quiet, force) -> None: if not quiet: print('=== Decompiling using FernFlower (silent) ===') t = time.time() - path = Path(f'./src/{version}-{side}-temp.jar') - fernflower = Path('./lib/fernflower.jar') - if path.exists() and fernflower.exists(): - path = path.resolve() - fernflower = fernflower.resolve() - subprocess.run(['java', - '-Xmx4G', - '-Xms1G', - '-jar', fernflower.__str__(), - '-hes=0', # hide empty super invocation deactivated (might clutter but allow following) - '-hdc=0', # hide empty default constructor deactivated (allow to track) - '-dgs=1', # decompile generic signatures activated (make sure we can follow types) - '-lit=1', # output numeric literals - '-asc=1', # encode non-ASCII characters in string and character - '-log=WARN', - path.__str__(), f'./src/{decompiled_version}/{side}' - ], check=True, capture_output=quiet) - if not quiet: - print(f'- Removing -> {version}-{side}-temp.jar') - os.remove(f'./src/{version}-{side}-temp.jar') - if not quiet: - print("Decompressing remapped jar to directory") - with zipfile.ZipFile(f'./src/{decompiled_version}/{side}/{version}-{side}-temp.jar') as z: - z.extractall(path=f'./src/{decompiled_version}/{side}') - t = time.time() - t - if not quiet: - print(f'Done in %.1fs (file was decompressed in {decompiled_version}/{side})' % t) - print('Remove Extra Jar file? (y/n): ') - response = input() or "y" - if response == 'y': - print(f'- Removing -> {decompiled_version}/{side}/{version}-{side}-temp.jar') - os.remove(f'./src/{decompiled_version}/{side}/{version}-{side}-temp.jar') - if force: - os.remove(f'./src/{decompiled_version}/{side}/{version}-{side}-temp.jar') - else: - if not quiet: - print(f'ERROR: Missing files: ./lib/fernflower.jar or ./src/{version}-{side}-temp.jar') - input("Aborting, press anything to exit") - sys.exit(-1) - - -def decompile_cfr(decompiled_version, version, side, quiet): + path = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + if not path.exists() or not path.is_file(): + raise RuntimeError(f'Missing file: {path}') + path = path.resolve() + + fernflower = (PATH_TO_ROOT_DIR / "lib" / "fernflower.jar") + if not fernflower.exists() or not fernflower.is_file(): + raise RuntimeError(f'Missing file: {fernflower}') + fernflower = fernflower.resolve() + + sideFolder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) + subprocess.run(['java', + '-Xmx4G', + '-Xms1G', + '-jar', fernflower.__str__(), + '-hes=0', # hide empty super invocation deactivated (might clutter but allow following) + '-hdc=0', # hide empty default constructor deactivated (allow to track) + '-dgs=1', # decompile generic signatures activated (make sure we can follow types) + '-lit=1', # output numeric literals + '-asc=1', # encode non-ASCII characters in string and character + '-log=WARN', + path.__str__(), sideFolder + ], check=True, capture_output=quiet) + if not quiet: + print(f'Removing {path}...') + os.remove(path) + if not quiet: + print("Decompressing remapped jar to directory...") + with zipfile.ZipFile(sideFolder / f"{version}-{side}-temp.jar") as z: + z.extractall(path=sideFolder) + t = time.time() - t + if not quiet: + print(f'Done in %.1fs (file was decompressed in {decompiled_version}/{side})' % t) + # TODO: Automate choice if auto mode is enabled + print('Remove Extra Jar file? (y/n): ') + response = input() or "y" + if response == 'y': + print(f'Removing {sideFolder / f"{version}-{side}-temp.jar"}...') + os.remove(sideFolder / f"{version}-{side}-temp.jar") + if force: + os.remove(sideFolder / f'{version}-{side}-temp.jar') + + +def decompile_cfr(decompiled_version: str, version: str, side: SideType, quiet) -> None: if not quiet: print('=== Decompiling using CFR (silent) ===') t = time.time() - path = Path(f'./src/{version}-{side}-temp.jar') - cfr = Path(f'./lib/cfr-{CFR_VERSION}.jar') - if path.exists() and cfr.exists(): - path = path.resolve() - cfr = cfr.resolve() - subprocess.run(['java', - '-Xmx4G', - '-Xms1G', - '-jar', cfr.__str__(), - path.__str__(), - '--outputdir', f'./src/{decompiled_version}/{side}', - '--caseinsensitivefs', 'true', - "--silent", "true" - ], check=True, capture_output=quiet) - if not quiet: - print(f'- Removing -> {version}-{side}-temp.jar') - print(f'- Removing -> summary.txt') - os.remove(f'./src/{version}-{side}-temp.jar') - os.remove(f'./src/{decompiled_version}/{side}/summary.txt') - if not quiet: - t = time.time() - t - print('Done in %.1fs' % t) - else: - if not quiet: - print(f'ERROR: Missing files: ./lib/cfr-{CFR_VERSION}.jar or ./src/{version}-{side}-temp.jar') - input("Aborting, press anything to exit") - sys.exit(-1) + path = (PATH_TO_ROOT_DIR / "src"/ f"{version}-{side}-temp.jar") + if not path.exists() or not path.is_file(): + raise RuntimeError(f'Missing file: {path}') + path = path.resolve() + + cfr = (PATH_TO_ROOT_DIR / "lib" / f"cfr-{CFR_VERSION}.jar") + if not cfr.exists() or not path.is_file(): + raise RuntimeError(f'Missing file: {cfr}') + cfr = cfr.resolve() + + sideFolder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) + subprocess.run(['java', + '-Xmx4G', + '-Xms1G', + '-jar', cfr.__str__(), + path.__str__(), + '--outputdir', sideFolder, + '--caseinsensitivefs', 'true', + "--silent", "true" + ], check=True, capture_output=quiet) + if not quiet: + print(f'Removing {path}...') + os.remove(path) + if not quiet: + print(f'Removing {sideFolder / "summary.txt"}...') + os.remove(sideFolder / "summary.txt") + if not quiet: + t = time.time() - t + print('Done in %.1fs' % t) -def remove_brackets(line, counter): + +def remove_brackets(line: str, counter: int) -> tuple[str, int]: while '[]' in line: # get rid of the array brackets while counting them counter += 1 line = line[:-2] return line, counter -def remap_file_path(path): +def remap_file_path(path: str) -> str: remap_primitives = {"int": "I", "double": "D", "boolean": "Z", "float": "F", "long": "J", "byte": "B", "short": "S", "char": "C", "void": "V"} return "L" + "/".join(path.split(".")) + ";" if path not in remap_primitives else remap_primitives[path] -def convert_mappings(version, side, quiet): - with open(f'mappings/{version}/{side}.txt', 'r') as inputFile: - file_name = {} +def convert_mappings(version: str, side: SideType, quiet) -> None: + versionSidePath = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.txt") + with open(versionSidePath, 'r') as inputFile: + file_name: dict[str, str] = {} for line in inputFile.readlines(): if line.startswith('#'): # comment at the top, could be stripped continue @@ -434,8 +412,7 @@ def convert_mappings(version, side, quiet): obf_name = obf_name.split(":")[0] file_name[remap_file_path(deobf_name)] = obf_name # save it to compare to put the Lb - with open(f'mappings/{version}/{side}.txt', 'r') as inputFile, open(f'mappings/{version}/{side}.tsrg', - 'w+') as outputFile: + with open(versionSidePath, 'r') as inputFile, open(PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.tsrg", 'w+') as outputFile: for line in inputFile.readlines(): if line.startswith('#'): # comment at the top, could be stripped continue @@ -452,8 +429,7 @@ def convert_mappings(version, side, quiet): array_length_type = 0 method_type, array_length_type = remove_brackets(method_type, array_length_type) - method_type = remap_file_path( - method_type) # remap the dots to / and add the L ; or remap to a primitives character + method_type = remap_file_path(method_type) # remap the dots to / and add the L ; or remap to a primitives character method_type = "L" + file_name[ method_type] + ";" if method_type in file_name else method_type # get the obfuscated name of the class if "." in method_type: # if the class is already packaged then change the name that the obfuscated gave @@ -477,7 +453,7 @@ def convert_mappings(version, side, quiet): variables = ["/".join(variable.split(".")) if "." in variable else variable for variable in variables] # if the class is already packaged then change the obfuscated name for i in range(len(variables)): # restore the array brackets upfront for each variable - for j in range(array_length_variables[i]): + for _ in range(array_length_variables[i]): if variables[i][-1] == ";": variables[i] = "[" + variables[i][:-1] + ";" else: @@ -495,60 +471,62 @@ def convert_mappings(version, side, quiet): print("Done !") -def make_paths(version, side, removal_bool, force, forceno): - path = Path(f'mappings/{version}') +def make_paths(version: str, side: SideType, removal_bool, force, forceno) -> str: + path = (PATH_TO_ROOT_DIR / "mappings" / version) if not path.exists(): path.mkdir(parents=True) else: if removal_bool: shutil.rmtree(path) path.mkdir(parents=True) - path = Path(f'versions/{version}') + + path = (PATH_TO_ROOT_DIR / "versions" / version) if not path.exists(): path.mkdir(parents=True) else: - path = Path(f'versions/{version}/version.json') + path = (path / "version.json") if path.is_file() and removal_bool: path.unlink() - if Path("versions").exists(): - path = Path(f'versions/version_manifest.json') + + if (PATH_TO_ROOT_DIR / "versions").exists(): + path = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") if path.is_file() and removal_bool: path.unlink() - path = Path(f'versions/{version}/{side}.jar') - if path.exists() and path.is_file() and removal_bool: + path = (PATH_TO_ROOT_DIR / "versions" / version / f"{side}.jar") + if path.is_file() and removal_bool: if force: - path = Path(f'versions/{version}') + path = (PATH_TO_ROOT_DIR / "versions" / version) shutil.rmtree(path) path.mkdir(parents=True) else: aw = input(f"versions/{version}/{side}.jar already exists, wipe it (w) or ignore (i) ? ") or "i" - path = Path(f'versions/{version}') + path = (PATH_TO_ROOT_DIR / "versions" / version) if aw == "w": shutil.rmtree(path) path.mkdir(parents=True) - path = Path(f'src/{version}/{side}') + path = (PATH_TO_ROOT_DIR / "src" / version / side) if not path.exists(): path.mkdir(parents=True) else: if force: - shutil.rmtree(Path(f"./src/{version}/{side}")) + shutil.rmtree(path) elif forceno: version = version + side + "_" + str(random.getrandbits(128)) + path = (PATH_TO_ROOT_DIR / "src" / version / side) else: - aw = input( - f"/src/{version}/{side} already exists, wipe it (w), create a new folder (n) or kill the process (k) ? ") + aw = input(f"/src/{version}/{side} already exists, wipe it (w), create a new folder (n) or kill the process (k) ? ") or "n" if aw == "w": - shutil.rmtree(Path(f"./src/{version}/{side}")) + shutil.rmtree(path) elif aw == "n": version = version + side + "_" + str(random.getrandbits(128)) + path = (PATH_TO_ROOT_DIR / "src" / version / side) else: sys.exit(-1) - path = Path(f'src/{version}/{side}') path.mkdir(parents=True) - path = Path(f'tmp/{version}/{side}') + path = (PATH_TO_ROOT_DIR / "tmp" / version / side) if not path.exists(): path.mkdir(parents=True) else: @@ -558,29 +536,25 @@ def make_paths(version, side, removal_bool, force, forceno): return version -def delete_dependencies(version, side): - path = f'./tmp/{version}/{side}' - - with zipfile.ZipFile(f'./src/{version}-{side}-temp.jar') as z: +def delete_dependencies(version: str, side: SideType) -> None: + path = (PATH_TO_ROOT_DIR / "tmp" / version / side) + tempjarPath = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + with zipfile.ZipFile(tempjarPath) as z: z.extractall(path=path) for _dir in [join(path, "com"), path]: for f in os.listdir(_dir): - if os.path.isdir(join(_dir, f)) and split(f)[-1] not in ['net', 'assets', 'data', 'mojang', 'com', - 'META-INF']: + if os.path.isdir(join(_dir, f)) and split(f)[-1] not in ['net', 'assets', 'data', 'mojang', 'com', 'META-INF']: shutil.rmtree(join(_dir, f)) - with zipfile.ZipFile(f'./src/{version}-{side}-temp.jar', 'w') as z: - for f in glob.iglob(f'{path}/**', recursive=True): - z.write(f, arcname=f[len(path) + 1:]) + with zipfile.ZipFile(tempjarPath, 'w') as z: + for f in glob.iglob(f'{path}{os.sep}**', recursive=True): + z.write(f, arcname=f[len(str(path)) + 1:]) def main(): check_java() snapshot, latest = get_latest_version() - if snapshot is None or latest is None: - print("Error getting latest versions, please refresh cache") - sys.exit(1) # for arguments parser = argparse.ArgumentParser(description='Decompile Minecraft source code') parser.add_argument('--mcversion', '-mcv', type=str, dest='mcversion', @@ -591,11 +565,11 @@ def main(): parser.add_argument('--clean', '-c', dest='clean', action='store_true', default=False, help=f"Clean old runs") parser.add_argument('--force', '-f', dest='force', action='store_true', default=False, - help=f"Force resolving conflict by replacing old files.") + help=f"Force resolve conflicts by replacing old files.") parser.add_argument('--forceno', '-fn', dest='forceno', action='store_false', default=True, - help=f"Force resolving conflict by creating new directories.") + help=f"Force resolve conflicts by creating new directories.") parser.add_argument('--decompiler', '-d', type=str, dest='decompiler', default="cfr", - help=f"Choose between fernflower and cfr.") + help=f"Choose between Fernflower and CFR.") parser.add_argument('--nauto', '-na', dest='nauto', action='store_true', default=False, help=f"Choose between auto and manual mode.") parser.add_argument('--download_mapping', '-dm', nargs='?', const=True, type=str2bool, dest='download_mapping', @@ -624,42 +598,40 @@ def main(): if args.mcversion: use_flags = True if not args.quiet: - print("Decompiling using official mojang mappings (Default option are in uppercase, you can just enter)") + print("Decompiling using official Mojang mappings (Default options are in uppercase, you can just press Enter):") if use_flags: removal_bool = args.clean else: - removal_bool = 1 if input("Do you want to clean up old runs? (y/N): ") in ["y", "yes"] else 0 + removal_bool = input("Do you want to clean up old runs? (y/N): ") in ["y", "yes"] if use_flags: decompiler = args.decompiler else: - decompiler = input("Please input you decompiler choice: fernflower or cfr (CFR/f): ") + decompiler = input("Please input your decompiler choice: Fernflower or CFR (CFR/f): ") decompiler = decompiler.lower() if decompiler.lower() in ["fernflower", "cfr", "f"] else "cfr" if use_flags: - version = args.mcversion + version: str | None = args.mcversion if version is None: - print( - "Error you should provide a version with --mcversion , use latest or snap if you dont know which one") - sys.exit(-1) + raise ValueError('You must provide a version with --mcversion ') else: - version = input(f"Please input a valid version starting from 19w36a (snapshot) and 1.14.4 (releases),\n" + - f"Use 'snap' for latest snapshot ({snapshot}) or 'latest' for latest version ({latest}) :") or latest + version = input(f"Please input a valid version starting from 19w36a (snapshot) or 1.14.4 (releases).\n" + + f"Use 'snap' for the latest snapshot ({snapshot}) or 'latest' for the latest version ({latest}) :") or latest if version in ["snap", "s", "snapshot"]: version = snapshot if version in ["latest", "l"]: version = latest if use_flags: - side = args.side + side: str = args.side else: side = input("Please select either client or server side (C/s) : ") - side = side.lower() if side.lower() in ["client", "server", "c", "s"] else CLIENT - side = CLIENT if side in ["client", "c"] else SERVER + side = side.lower() if side.lower() in ("client", "server", "c", "s") else CLIENT + side = CLIENT if side in ("client", "c") else SERVER decompiled_version = make_paths(version, side, removal_bool, args.force, args.forceno) get_global_manifest(args.quiet) get_version_manifest(version, args.quiet) if use_flags: r = not args.nauto else: - r = input("Auto Mode? (Y/n): ") or "y" + r = input("Auto mode? (Y/n): ") or "y" r = r.lower() == "y" if r: get_mappings(version, side, args.quiet) @@ -672,9 +644,8 @@ def main(): decompile_fern_flower(decompiled_version, version, side, args.quiet, args.force) if not args.quiet: print("===FINISHED===") - print(f"output is in /src/{version}") - input("Press Enter key to exit") - sys.exit(0) + print(f"Output is in /src/{decompiled_version}") + return if use_flags: r = args.download_mapping @@ -728,10 +699,7 @@ def main(): decompile_fern_flower(decompiled_version, version, side, args.quiet, args.force) if not args.quiet: print("===FINISHED===") - print(f"output is in /src/{decompiled_version}") - input("Press Enter key to exit") - else: - sys.exit(0) + print(f"Output is in /src/{decompiled_version}") if __name__ == "__main__": From 66ce2ecf342b01c141b13da7c8993da88c2f76e3 Mon Sep 17 00:00:00 2001 From: Nel-S <75831544+Nel-S@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:43:29 -0700 Subject: [PATCH 2/3] Significantly copyedit Readme (but no new information was really added) --- README.md | 62 +++++++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 673ad47..9b22e21 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,28 @@ # DecompilerMC ---- -**What is this for?** +This tool automatically decompiles and remaps specific Minecraft versions. (Specifically, it converts Mojang's mappings from their proguard format to the tsrg format. SpecialSource then uses that and remaps the client jar, which is then decompiled either with CFR (code only) or Fernflower (assets and code).) -This tool will help you convert mappings from mojang from their proguard format to the tsrg format that then can be used directly with specialsource which will then remap the client jar. Once that done it can be decompiled either with cfr (code only) or fernflower (assets and code). +Your output will be readable/executable code similar to ModCoderPack or other decompilers. -Of course we provide all that toolchain directly so your output will be readable (and soon executable) code as you could get with MCP (ModCoderPack) +## Prerequisites ---- -**Important Note** +You will need +- an Internet connection to download the mappings. You can obviously put them in the respective folder if you have them physically. +- Windows, MacOS, or Linux. +- A Java runtime inside your path (Java 8 should be good). -You need an internet connection to download the mappings, you can ofc put them in the respective folder if you have them physically +You can run this directly with Python 3.7+ with `python3 main.py`. CFR decompilation takes approximately 60s and fernflower takes roughly 200s. The code will then be inside the folder called `./src//`; you can find the jar and the version manifest in the `./versions/` directory. -We support Windows, MacOS and linux +The `./tmp/` directory can be removed without impact. -You need a java runtime inside your path (Java 8 should be good) - -CFR decompilation is approximately 60s and fernflower takes roughly 200s, please give it time - -You can run it directly with python 3.7+ with `python3 main.py` - -You can find the jar and the version manifest in the `./versions/` directory - -The code will then be inside the folder called `./src//` - -The `./tmp/` directory can be removed without impact - -There is a common release here: https://github.com/hube12/DecompilerMC/releases/latest for all version +There is a common release here: https://github.com/hube12/DecompilerMC/releases/latest for all versions. ---- -You can use arguments instead of terminal based choice, this is not required but once you pass a mcversion it will start the process - -We recommend using -q everytime otherwise it might ask stdin questions. - -By default we employ the nice guy strategy which is if the folder exist we create a new random one, please consider using -f, -if you actually need a specific path. - -Examples: -- Decompile latest release without any output: `python3 main.py --mcv latest -q` -- Decompile latest snapshot server side with output: `python3 main.py --mcversion snap --side server` -- Decompile 1.14.4 client side with output and not automatic with forcing delete of old runs: `python3 main.py -mcv 1.14.4 -s client -na -f -rmap -rjar -dm -dj -dd -dec -q -c` - +## Command-line Arguments (Optional) +You can use arguments instead of terminal-based choices. This is not required, but will automatically start if a mcversion is passed. ```bash - usage: main.py [-h] [--mcversion MCVERSION] [--side SIDE] [--clean] [--force] [--forceno] [--decompiler DECOMPILER] [--nauto] [--download_mapping DOWNLOAD_MAPPING] @@ -52,8 +30,6 @@ usage: main.py [-h] [--mcversion MCVERSION] [--side SIDE] [--clean] [--force] [--download_jar [DOWNLOAD_JAR]] [--remap_jar [REMAP_JAR]] [--delete_dep [DELETE_DEP]] [--decompile [DECOMPILE]] [--quiet] -Decompile Minecraft source code - optional arguments: -h, --help show this help message and exit --mcversion MCVERSION, -mcv MCVERSION @@ -64,8 +40,8 @@ optional arguments: --side SIDE, -s SIDE The side you want to decompile (either client or server) --clean, -c Clean old runs - --force, -f Force resolving conflict by replacing old files. - --forceno, -fn Force resolving conflict by creating new directories. + --force, -f Force resolve conflicts by replacing old files. (Use if a specific path is necessary) + --forceno, -fn Force resolve conflicts by creating new directories. --decompiler DECOMPILER, -d DECOMPILER Choose between fernflower and cfr. --nauto, -na Choose between auto and manual mode. @@ -81,13 +57,17 @@ optional arguments: Delete the dependencies (only if auto off) --decompile [DECOMPILE], -dec [DECOMPILE] Decompile (only if auto off) - --quiet, -q Doesn't display the messages + --quiet, -q Doesn't display messages (recommended) ``` ----- +Examples: +- Decompile latest release without any output: `python3 main.py --mcv latest -q` +- Decompile latest snapshot server side with output: `python3 main.py --mcversion snap --side server` +- Decompile 1.14.4 client side with output and not automatic with forcing delete of old runs: `python3 main.py -mcv 1.14.4 -s client -na -f -rmap -rjar -dm -dj -dd -dec -q -c` -Build command (for executable): +---- +To build as an executable, the commands are ```python pip install pyinstaller pyinstaller main.py --distpath build --onefile From 830b24a1be7b878bf59894051f57e883c97d7b99 Mon Sep 17 00:00:00 2001 From: Neil Date: Sat, 2 Nov 2024 14:45:21 +0100 Subject: [PATCH 3/3] More fixes and linting --- main.py | 132 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/main.py b/main.py index 4d78068..484e8f5 100644 --- a/main.py +++ b/main.py @@ -103,14 +103,16 @@ def check_java() -> None: results.append(which('java', path='/opt')) results = [path for path in results if path is not None] if not results: - raise RuntimeError('Java JDK is not installed! Please install a Java JDK from https://java.oracle.com, or install OpenJDK.') + raise RuntimeError( + 'Java JDK is not installed! Please install a Java JDK from https://java.oracle.com, or install OpenJDK.') def get_global_manifest(quiet) -> None: versionManifsetPath = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") if versionManifsetPath.is_file(): if not quiet: - print(f"Manifest already exists; not downloading again. If another manifest is wanted, please delete manually before running the program (location: {versionManifsetPath}).") + print( + f"Manifest already exists; not downloading again. If another manifest is wanted, please delete manually before running the program (location: {versionManifsetPath}).") return download_file(MANIFEST_LOCATION, versionManifsetPath, quiet) @@ -150,21 +152,22 @@ def get_latest_version() -> tuple[str, str]: def get_version_manifest(target_version: str, quiet) -> None: - versionPath = (PATH_TO_ROOT_DIR / "versions" / target_version / "version.json") - if versionPath.is_file(): + version_path = (PATH_TO_ROOT_DIR / "versions" / target_version / "version.json") + if version_path.is_file(): if not quiet: - print(f"Version manifest already exists; not downloading again. If another version manifest is wanted, please delete manually before running the program (location: {versionPath}).") + print( + f"Version manifest already exists; not downloading again. If another version manifest is wanted, please delete manually before running the program (location: {version_path}).") return path_to_json = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") - if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError(f'Missing manifest file: {path_to_json}') + if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError( + f'Missing manifest file: {path_to_json}') path_to_json = path_to_json.resolve() with open(path_to_json) as f: versions = json.load(f)["versions"] for version in versions: if version.get("id") and version.get("id") == target_version and version.get("url"): - download_file(version.get("url"), versionPath, quiet) + download_file(version.get("url"), version_path, quiet) break - def sha256(fname: Union[Union[str, bytes], int]) -> str: @@ -177,23 +180,27 @@ def sha256(fname: Union[Union[str, bytes], int]) -> str: def get_version_jar(target_version: str, side: SideType, quiet) -> None: path_to_json = (PATH_TO_ROOT_DIR / "versions" / target_version / "version.json") - targetSidePath = (PATH_TO_ROOT_DIR / "versions" / target_version / f"{side}.jar") - if targetSidePath.is_file(): + target_side_path = (PATH_TO_ROOT_DIR / "versions" / target_version / f"{side}.jar") + if target_side_path.is_file(): if not quiet: - print(f"Version jar already exists; not downloading again. If another version jar is wanted, please delete manually before running the program (location: {targetSidePath}).") + print( + f"Version jar already exists; not downloading again. If another version jar is wanted, please delete manually before running the program (location: {target_side_path}).") return - if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError(f'Missing manifest file: {path_to_json}') + if not path_to_json.exists() or not path_to_json.is_file(): raise RuntimeError( + f'Missing manifest file: {path_to_json}') path_to_json = path_to_json.resolve() with open(path_to_json) as f: jsn = json.load(f) - if not jsn.get("downloads") or not jsn.get("downloads").get(side) or not jsn.get("downloads").get(side).get("url"): + if not jsn.get("downloads") or not jsn.get("downloads").get(side) or not jsn.get("downloads").get(side).get( + "url"): raise RuntimeError("Could not download jar, missing fields") - download_file(jsn.get("downloads").get(side).get("url"), targetSidePath, quiet) + download_file(jsn.get("downloads").get(side).get("url"), target_side_path, quiet) # In case the server is newer than 21w39a you need to actually extract it first from the archive if side == SERVER: - if not targetSidePath.exists(): - raise RuntimeError(f"Jar was maybe downloaded but not located, this is a failure, check path at {targetSidePath}") - with zipfile.ZipFile(targetSidePath, mode="r") as z: + if not target_side_path.exists(): + raise RuntimeError( + f"Jar was maybe downloaded but not located, this is a failure, check path at {target_side_path}") + with zipfile.ZipFile(target_side_path, mode="r") as z: content = None try: content = z.read(Path(f"META-INF", "versions.list")) @@ -203,12 +210,14 @@ def get_version_jar(target_version: str, side: SideType, quiet) -> None: if content is not None: element = content.split(b"\t") if len(element) != 3: - raise RuntimeError(f"Jar should be extracted but version list is not in the correct format, expected 3 fields, got {len(element)} for {content}") + raise RuntimeError( + f"Jar should be extracted but version list is not in the correct format, expected 3 fields, got {len(element)} for {content}") version_hash = element[0].decode() version = element[1].decode() path = element[2].decode() if version != target_version and not quiet: - print(f"Warning: received version ({version}) does not match the targeted version ({target_version}).") + print( + f"Warning: received version ({version}) does not match the targeted version ({target_version}).") new_jar_path = (PATH_TO_ROOT_DIR / "versions" / target_version) try: new_jar_path = z.extract(Path("META-INF", "versions", path), new_jar_path) @@ -218,9 +227,10 @@ def get_version_jar(target_version: str, side: SideType, quiet) -> None: raise RuntimeError(f"New {side} jar could not be extracted from archive at {new_jar_path}.") file_hash = sha256(new_jar_path) if file_hash != version_hash: - raise RuntimeError(f"Extracted file's hash ({file_hash}) and expected hash ({version_hash}) did not match.") + raise RuntimeError( + f"Extracted file's hash ({file_hash}) and expected hash ({version_hash}) did not match.") try: - shutil.move((PATH_TO_ROOT_DIR / new_jar_path), targetSidePath) + shutil.move((PATH_TO_ROOT_DIR / new_jar_path), target_side_path) shutil.rmtree((PATH_TO_ROOT_DIR / "versions" / target_version / "META-INF")) except Exception as e: raise RuntimeError("Exception while removing the temp file", e) @@ -232,7 +242,8 @@ def get_mappings(version: str, side: SideType, quiet) -> None: versionSidePath = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.txt") if versionSidePath.is_file(): if not quiet: - print(f"Mappings already exist; not downloading again. If other mappings are wanted, please delete manually before running the program (location: {versionSidePath}).") + print( + f"Mappings already exist; not downloading again. If other mappings are wanted, please delete manually before running the program (location: {versionSidePath}).") return path_to_json = (PATH_TO_ROOT_DIR / "versions" / version / "version.json") if not path_to_json.exists() or not path_to_json.is_file(): @@ -245,19 +256,21 @@ def get_mappings(version: str, side: SideType, quiet) -> None: url = jfile['downloads'] if side == CLIENT: # client: if 'client_mappings' not in url or 'url' not in url['client_mappings']: - #TODO: Clean up failed run before raising + # TODO: Clean up failed run before raising raise RuntimeError(f'Could not find client mappings for {version}') url = url['client_mappings']['url'] elif side == SERVER: # server if 'server_mappings' not in url or 'url' not in url['server_mappings']: - #TODO: Clean up failed run before raising + # TODO: Clean up failed run before raising raise RuntimeError(f'Could not find server mappings for {version}') url = url['server_mappings']['url'] else: raise RuntimeError('Type not recognized.') if not quiet: print(f'Downloading the mappings for {version}...') - download_file(url, (PATH_TO_ROOT_DIR / "mappings" / version / f"{'client' if side == CLIENT else 'server'}.txt"), quiet) + download_file(url, + (PATH_TO_ROOT_DIR / "mappings" / version / f"{'client' if side == CLIENT else 'server'}.txt"), + quiet) def remap(version: str, side: SideType, quiet) -> None: @@ -284,21 +297,21 @@ def remap(version: str, side: SideType, quiet) -> None: raise RuntimeError(f'Missing file: {mapp}') mapp = mapp.resolve() - specialsource = (PATH_TO_ROOT_DIR / "lib"/ f"SpecialSource-{SPECIAL_SOURCE_VERSION}.jar") - if not specialsource.exists() or not specialsource.is_file(): - raise RuntimeError(f'Missing file: {specialsource}') - specialsource = specialsource.resolve() - outJarPath = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + special_source_path = (PATH_TO_ROOT_DIR / "lib" / f"SpecialSource-{SPECIAL_SOURCE_VERSION}.jar") + if not special_source_path.exists() or not special_source_path.is_file(): + raise RuntimeError(f'Missing file: {special_source_path}') + special_source_path = special_source_path.resolve() + out_jar_path = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") subprocess.run(['java', - '-jar', specialsource.__str__(), + '-jar', special_source_path.__str__(), '--in-jar', path.__str__(), - '--out-jar', outJarPath, + '--out-jar', out_jar_path, '--srg-in', mapp.__str__(), "--kill-lvt" # kill snowmen ], check=True, capture_output=quiet) if not quiet: - print(f'Created {outJarPath}.') + print(f'Created {out_jar_path}.') t = time.time() - t print('Done in %.1fs' % t) @@ -318,7 +331,7 @@ def decompile_fern_flower(decompiled_version: str, version: str, side: SideType, raise RuntimeError(f'Missing file: {fernflower}') fernflower = fernflower.resolve() - sideFolder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) + side_folder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) subprocess.run(['java', '-Xmx4G', '-Xms1G', @@ -329,15 +342,15 @@ def decompile_fern_flower(decompiled_version: str, version: str, side: SideType, '-lit=1', # output numeric literals '-asc=1', # encode non-ASCII characters in string and character '-log=WARN', - path.__str__(), sideFolder + path.__str__(), side_folder ], check=True, capture_output=quiet) if not quiet: print(f'Removing {path}...') os.remove(path) if not quiet: print("Decompressing remapped jar to directory...") - with zipfile.ZipFile(sideFolder / f"{version}-{side}-temp.jar") as z: - z.extractall(path=sideFolder) + with zipfile.ZipFile(side_folder / f"{version}-{side}-temp.jar") as z: + z.extractall(path=side_folder) t = time.time() - t if not quiet: print(f'Done in %.1fs (file was decompressed in {decompiled_version}/{side})' % t) @@ -345,10 +358,10 @@ def decompile_fern_flower(decompiled_version: str, version: str, side: SideType, print('Remove Extra Jar file? (y/n): ') response = input() or "y" if response == 'y': - print(f'Removing {sideFolder / f"{version}-{side}-temp.jar"}...') - os.remove(sideFolder / f"{version}-{side}-temp.jar") + print(f'Removing {side_folder / f"{version}-{side}-temp.jar"}...') + os.remove(side_folder / f"{version}-{side}-temp.jar") if force: - os.remove(sideFolder / f'{version}-{side}-temp.jar') + os.remove(side_folder / f'{version}-{side}-temp.jar') def decompile_cfr(decompiled_version: str, version: str, side: SideType, quiet) -> None: @@ -356,8 +369,8 @@ def decompile_cfr(decompiled_version: str, version: str, side: SideType, quiet) print('=== Decompiling using CFR (silent) ===') t = time.time() - path = (PATH_TO_ROOT_DIR / "src"/ f"{version}-{side}-temp.jar") - if not path.exists() or not path.is_file(): + path = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + if not path.exists() or not path.is_file(): raise RuntimeError(f'Missing file: {path}') path = path.resolve() @@ -366,13 +379,13 @@ def decompile_cfr(decompiled_version: str, version: str, side: SideType, quiet) raise RuntimeError(f'Missing file: {cfr}') cfr = cfr.resolve() - sideFolder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) + side_folder = (PATH_TO_ROOT_DIR / "src" / decompiled_version / side) subprocess.run(['java', '-Xmx4G', '-Xms1G', '-jar', cfr.__str__(), path.__str__(), - '--outputdir', sideFolder, + '--outputdir', side_folder, '--caseinsensitivefs', 'true', "--silent", "true" ], check=True, capture_output=quiet) @@ -380,8 +393,8 @@ def decompile_cfr(decompiled_version: str, version: str, side: SideType, quiet) print(f'Removing {path}...') os.remove(path) if not quiet: - print(f'Removing {sideFolder / "summary.txt"}...') - os.remove(sideFolder / "summary.txt") + print(f'Removing {side_folder / "summary.txt"}...') + os.remove(side_folder / "summary.txt") if not quiet: t = time.time() - t print('Done in %.1fs' % t) @@ -401,8 +414,8 @@ def remap_file_path(path: str) -> str: def convert_mappings(version: str, side: SideType, quiet) -> None: - versionSidePath = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.txt") - with open(versionSidePath, 'r') as inputFile: + version_side_path = (PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.txt") + with open(version_side_path, 'r') as inputFile: file_name: dict[str, str] = {} for line in inputFile.readlines(): if line.startswith('#'): # comment at the top, could be stripped @@ -412,7 +425,8 @@ def convert_mappings(version: str, side: SideType, quiet) -> None: obf_name = obf_name.split(":")[0] file_name[remap_file_path(deobf_name)] = obf_name # save it to compare to put the Lb - with open(versionSidePath, 'r') as inputFile, open(PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.tsrg", 'w+') as outputFile: + with open(version_side_path, 'r') as inputFile, open(PATH_TO_ROOT_DIR / "mappings" / version / f"{side}.tsrg", + 'w+') as outputFile: for line in inputFile.readlines(): if line.startswith('#'): # comment at the top, could be stripped continue @@ -429,7 +443,8 @@ def convert_mappings(version: str, side: SideType, quiet) -> None: array_length_type = 0 method_type, array_length_type = remove_brackets(method_type, array_length_type) - method_type = remap_file_path(method_type) # remap the dots to / and add the L ; or remap to a primitives character + method_type = remap_file_path( + method_type) # remap the dots to / and add the L ; or remap to a primitives character method_type = "L" + file_name[ method_type] + ";" if method_type in file_name else method_type # get the obfuscated name of the class if "." in method_type: # if the class is already packaged then change the name that the obfuscated gave @@ -487,7 +502,7 @@ def make_paths(version: str, side: SideType, removal_bool, force, forceno) -> st path = (path / "version.json") if path.is_file() and removal_bool: path.unlink() - + if (PATH_TO_ROOT_DIR / "versions").exists(): path = (PATH_TO_ROOT_DIR / "versions" / "version_manifest.json") if path.is_file() and removal_bool: @@ -516,7 +531,8 @@ def make_paths(version: str, side: SideType, removal_bool, force, forceno) -> st version = version + side + "_" + str(random.getrandbits(128)) path = (PATH_TO_ROOT_DIR / "src" / version / side) else: - aw = input(f"/src/{version}/{side} already exists, wipe it (w), create a new folder (n) or kill the process (k) ? ") or "n" + aw = input( + f"/src/{version}/{side} already exists, wipe it (w), create a new folder (n) or kill the process (k) ? ") or "n" if aw == "w": shutil.rmtree(path) elif aw == "n": @@ -538,16 +554,17 @@ def make_paths(version: str, side: SideType, removal_bool, force, forceno) -> st def delete_dependencies(version: str, side: SideType) -> None: path = (PATH_TO_ROOT_DIR / "tmp" / version / side) - tempjarPath = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") - with zipfile.ZipFile(tempjarPath) as z: + temp_jar_path = (PATH_TO_ROOT_DIR / "src" / f"{version}-{side}-temp.jar") + with zipfile.ZipFile(temp_jar_path) as z: z.extractall(path=path) for _dir in [join(path, "com"), path]: for f in os.listdir(_dir): - if os.path.isdir(join(_dir, f)) and split(f)[-1] not in ['net', 'assets', 'data', 'mojang', 'com', 'META-INF']: + if os.path.isdir(join(_dir, f)) and split(f)[-1] not in ['net', 'assets', 'data', 'mojang', 'com', + 'META-INF']: shutil.rmtree(join(_dir, f)) - with zipfile.ZipFile(tempjarPath, 'w') as z: + with zipfile.ZipFile(temp_jar_path, 'w') as z: for f in glob.iglob(f'{path}{os.sep}**', recursive=True): z.write(f, arcname=f[len(str(path)) + 1:]) @@ -598,7 +615,8 @@ def main(): if args.mcversion: use_flags = True if not args.quiet: - print("Decompiling using official Mojang mappings (Default options are in uppercase, you can just press Enter):") + print( + "Decompiling using official Mojang mappings (Default options are in uppercase, you can just press Enter):") if use_flags: removal_bool = args.clean else: