From 521122fd4f267f3a6f95321d3e14e61fa3877c27 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Tue, 10 May 2022 21:00:53 -0700 Subject: [PATCH] Minecraft Version support (#458) * add support for other java/forge versions * fix fetching correct mod for specified version. * add support for other java/forge versions * fix fetching correct mod for specified version. * convert MinecraftClient.py to read forge versions from Randomizer Mod Repo. * add minecraft_versions.json to gitignore. * remove redundant json import * update host to release. add forge checking, fixed duplicated code due to merge. * clerify that beta channel will most likely make games no longer playable on release channel * convert commetns to docstrings. --- .gitignore | 2 + MinecraftClient.py | 154 ++++++++++++++++++++++++----------- Utils.py | 3 +- host.yaml | 5 +- worlds/minecraft/__init__.py | 3 - 5 files changed, 115 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 3bdaaaba..b0526255 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ MANIFEST # Installer logs pip-log.txt pip-delete-this-directory.txt +installer.log # Unit test / coverage reports htmlcov/ @@ -154,6 +155,7 @@ cython_debug/ #minecraft server stuff jdk*/ minecraft*/ +minecraft_versions.json #pyenv .python-version diff --git a/MinecraftClient.py b/MinecraftClient.py index 5c0fc535..d75f6d0f 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -1,4 +1,5 @@ import argparse +import json import os import sys import re @@ -17,7 +18,6 @@ atexit.register(input, "Press enter to exit.") # 1 or more digits followed by m or g, then optional b max_heap_re = re.compile(r"^\d+[mMgG][bB]?$") -forge_version = "1.17.1-37.1.1" is_windows = sys.platform in ("win32", "cygwin", "msys") @@ -34,8 +34,8 @@ def prompt_yes_no(prompt): print('Please respond with "y" or "n".') -# Create mods folder if needed; find AP randomizer jar; return None if not found. def find_ap_randomizer_jar(forge_dir): + """Create mods folder if needed; find AP randomizer jar; return None if not found.""" mods_dir = os.path.join(forge_dir, 'mods') if os.path.isdir(mods_dir): for entry in os.scandir(mods_dir): @@ -49,8 +49,8 @@ def find_ap_randomizer_jar(forge_dir): return None -# Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory. def replace_apmc_files(forge_dir, apmc_file): + """Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory.""" if apmc_file is None: return apdata_dir = os.path.join(forge_dir, 'APData') @@ -72,27 +72,21 @@ def replace_apmc_files(forge_dir, apmc_file): def read_apmc_file(apmc_file): from base64 import b64decode - import json with open(apmc_file, 'r') as f: - data = json.loads(b64decode(f.read())) - return data + return json.loads(b64decode(f.read())) -# Check mod version, download new mod from GitHub releases page if needed. -def update_mod(forge_dir, apmc_file, get_prereleases=False): +def update_mod(forge_dir, minecraft_version: str, get_prereleases=False): + """Check mod version, download new mod from GitHub releases page if needed. """ ap_randomizer = find_ap_randomizer_jar(forge_dir) - if apmc_file is not None: - data = read_apmc_file(apmc_file) - minecraft_version = data.get('minecraft_version', '') - client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases" resp = requests.get(client_releases_endpoint) if resp.status_code == 200: # OK try: - latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and - (apmc_file is None or minecraft_version in release['assets'][0]['name']), + latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and + (minecraft_version in release['assets'][0]['name']), resp.json())) if ap_randomizer != latest_release['assets'][0]['name']: logging.info(f"A new release of the Minecraft AP randomizer mod was found: " @@ -128,8 +122,8 @@ def update_mod(forge_dir, apmc_file, get_prereleases=False): sys.exit(0) -# Check if the EULA is agreed to, and prompt the user to read and agree if necessary. def check_eula(forge_dir): + """Check if the EULA is agreed to, and prompt the user to read and agree if necessary.""" eula_path = os.path.join(forge_dir, "eula.txt") if not os.path.isfile(eula_path): # Create eula.txt @@ -152,17 +146,18 @@ def check_eula(forge_dir): sys.exit(0) -# get the current JDK16 -def find_jdk_dir() -> str: +def find_jdk_dir(version: str) -> str: + """get the specified versions jdk directory""" for entry in os.listdir(): - if os.path.isdir(entry) and entry.startswith("jdk16"): + if os.path.isdir(entry) and entry.startswith(f"jdk{version}"): return os.path.abspath(entry) -# get the java exe location -def find_jdk() -> str: +def find_jdk(version: str) -> str: + """get the java exe location""" + if is_windows: - jdk = find_jdk_dir() + jdk = find_jdk_dir(version) jdk_exe = os.path.join(jdk, "bin", "java.exe") if os.path.isfile(jdk_exe): return jdk_exe @@ -173,16 +168,17 @@ def find_jdk() -> str: return jdk_exe -# Download Corretto 16 (Amazon JDK) -def download_java(): - jdk = find_jdk_dir() +def download_java(java: str): + """Download Corretto (Amazon JDK)""" + + jdk = find_jdk_dir(java) if jdk is not None: print(f"Removing old JDK...") from shutil import rmtree rmtree(jdk) print(f"Downloading Java...") - jdk_url = "https://corretto.aws/downloads/latest/amazon-corretto-16-x64-windows-jdk.zip" + jdk_url = f"https://corretto.aws/downloads/latest/amazon-corretto-{java}-x64-windows-jdk.zip" resp = requests.get(jdk_url) if resp.status_code == 200: # OK print(f"Extracting...") @@ -197,9 +193,10 @@ def download_java(): sys.exit(0) -# download and install forge -def install_forge(directory: str): - jdk = find_jdk() +def install_forge(directory: str, forge_version: str, java_version: str): + """download and install forge""" + + jdk = find_jdk(java_version) if jdk is not None: print(f"Downloading Forge {forge_version}...") forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar" @@ -211,20 +208,20 @@ def install_forge(directory: str): with open(forge_install_jar, 'wb') as f: f.write(resp.content) print(f"Installing Forge...") - argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""]) + argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar + "\"", "--installServer", "\"" + directory + "\""]) install_process = Popen(argstring, shell=not is_windows) install_process.wait() os.remove(forge_install_jar) -# Run the Forge server. Return process object -def run_forge_server(forge_dir: str, heap_arg): +def run_forge_server(forge_dir: str, java_version: str, heap_arg: str) -> Popen: + """Run the Forge server.""" - java_exe = find_jdk() + java_exe = find_jdk(java_version) if not os.path.isfile(java_exe): java_exe = "java" # try to fall back on java in the PATH - heap_arg = max_heap_re.match(max_heap).group() + heap_arg = max_heap_re.match(heap_arg).group() if heap_arg[-1] in ['b', 'B']: heap_arg = heap_arg[:-1] heap_arg = "-Xmx" + heap_arg @@ -242,46 +239,109 @@ def run_forge_server(forge_dir: str, heap_arg): return Popen(argstring, shell=not is_windows) +def get_minecraft_versions(version, release_channel="release"): + version_file_endpoint = "https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json" + resp = requests.get(version_file_endpoint) + local = False + if resp.status_code == 200: # OK + try: + data = resp.json() + except requests.exceptions.JSONDecodeError: + logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).") + local = True + else: + logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).") + local = True + + if local: + with open(Utils.local_path("minecraft_versions.json"), 'r') as f: + data = json.load(f) + else: + with open(Utils.local_path("minecraft_versions.json"), 'w') as f: + json.dump(data, f) + + try: + if version: + return next(filter(lambda entry: entry["version"] == version, data[release_channel])) + else: + return resp.json()[release_channel][0] + except StopIteration: + logging.error(f"No compatible mod version found for client version {version}.") + + +def is_correct_forge(forge_dir) -> bool: + if os.path.isdir(os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version)): + return True + return False + + if __name__ == '__main__': Utils.init_logging("MinecraftClient") parser = argparse.ArgumentParser() parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)") - parser.add_argument('--install', '-i', dest='install', default=False, action='store_true', - help="Download and install Java and the Forge server. Does not launch the client afterwards.") - parser.add_argument('--prerelease', default=False, action='store_true', - help="Auto-update prerelease versions.") + parser.add_argument('--install', '-i', dest='install', default=False, action='store_true', + help="Download and install Java and the Forge server. Does not launch the client afterwards.") + parser.add_argument('--release_channel', '-r', dest="channel", type=str, action='store', + help="Specify release channel to use.") + parser.add_argument('--java', '-j', metavar='17', dest='java', type=str, default=False, action='store', + help="specify java version.") + parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store', + help="specify forge version. (Minecraft Version-Forge Version)") args = parser.parse_args() apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None # Change to executable's working directory os.chdir(os.path.abspath(os.path.dirname(sys.argv[0]))) - + options = Utils.get_options() + channel = args.channel or options["minecraft_options"]["release_channel"] + apmc_data = None + data_version = None + + if apmc_file is not None: + apmc_data = read_apmc_file(apmc_file) + data_version = apmc_data.get('client_version', '') + + versions = get_minecraft_versions(data_version, channel) + forge_dir = options["minecraft_options"]["forge_directory"] max_heap = options["minecraft_options"]["max_heap_size"] + forge_version = args.forge or versions["forge"] + java_version = args.java or versions["java"] + java_dir = find_jdk_dir(java_version) if args.install: if is_windows: print("Installing Java and Minecraft Forge") - download_java() + download_java(java_version) else: print("Installing Minecraft Forge") - install_forge(forge_dir) + install_forge(forge_dir, forge_version, java_version) sys.exit(0) - if apmc_file is not None and not os.path.isfile(apmc_file): - raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.") - if not os.path.isdir(forge_dir): - if prompt_yes_no("Did not find forge directory. Download and install forge now?"): - install_forge(forge_dir) + if apmc_data is None: + raise FileNotFoundError(f"APMC file does not exist or is inaccessible at the given location ({apmc_file})") + + if is_windows: + if java_dir is None or not os.path.isdir(java_dir): + if prompt_yes_no("Did not find java directory. Download and install java now?"): + download_java(java_version) + java_dir = find_jdk_dir(java_version) + if java_dir is None or not os.path.isdir(java_dir): + raise NotADirectoryError(f"Path {java_dir} does not exist or could not be accessed.") + + if not is_correct_forge(forge_dir): + if prompt_yes_no(f"Did not find forge version {forge_version} download and install it now?"): + install_forge(forge_dir, forge_version, java_version) if not os.path.isdir(forge_dir): raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.") + if not max_heap_re.match(max_heap): raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.") - update_mod(forge_dir, apmc_file, args.prerelease) + update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release") replace_apmc_files(forge_dir, apmc_file) check_eula(forge_dir) - server_process = run_forge_server(forge_dir, max_heap) + server_process = run_forge_server(forge_dir, java_version, max_heap) server_process.wait() diff --git a/Utils.py b/Utils.py index 5cfe673f..fb2289d3 100644 --- a/Utils.py +++ b/Utils.py @@ -263,7 +263,8 @@ def get_default_options() -> dict: }, "minecraft_options": { "forge_directory": "Minecraft Forge server", - "max_heap_size": "2G" + "max_heap_size": "2G", + "release_channel": "release" }, "oot_options": { "rom_file": "The Legend of Zelda - Ocarina of Time.z64", diff --git a/host.yaml b/host.yaml index a96d7f1b..cfa49d4d 100644 --- a/host.yaml +++ b/host.yaml @@ -105,7 +105,10 @@ factorio_options: minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" -oot_options: + # release channel, currently "release", or "beta" + # any games played on the "beta" channel have a high likelihood of no longer working on the "release" channel. + release_channel: "release" +oot_options: # File name of the OoT v1.0 ROM rom_file: "The Legend of Zelda - Ocarina of Time.z64" # Set this to false to never autostart a rom (such as after patching) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index af57296a..27f89554 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -14,8 +14,6 @@ from .Options import minecraft_options from ..AutoWorld import World, WebWorld client_version = 7 -minecraft_version = "1.17.1" - class MinecraftWebWorld(WebWorld): theme = "jungle" @@ -47,7 +45,6 @@ class MinecraftWorld(World): 'player_name': self.world.get_player_name(self.player), 'player_id': self.player, 'client_version': client_version, - 'minecraft_version': minecraft_version, 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}, 'advancement_goal': self.world.advancement_goal[self.player].value, 'egg_shards_required': min(self.world.egg_shards_required[self.player].value,