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.
This commit is contained in:
parent
86933d8150
commit
521122fd4f
|
@ -77,6 +77,7 @@ MANIFEST
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
|
installer.log
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
@ -154,6 +155,7 @@ cython_debug/
|
||||||
#minecraft server stuff
|
#minecraft server stuff
|
||||||
jdk*/
|
jdk*/
|
||||||
minecraft*/
|
minecraft*/
|
||||||
|
minecraft_versions.json
|
||||||
|
|
||||||
#pyenv
|
#pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
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
|
# 1 or more digits followed by m or g, then optional b
|
||||||
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
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")
|
is_windows = sys.platform in ("win32", "cygwin", "msys")
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,8 +34,8 @@ def prompt_yes_no(prompt):
|
||||||
print('Please respond with "y" or "n".')
|
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):
|
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')
|
mods_dir = os.path.join(forge_dir, 'mods')
|
||||||
if os.path.isdir(mods_dir):
|
if os.path.isdir(mods_dir):
|
||||||
for entry in os.scandir(mods_dir):
|
for entry in os.scandir(mods_dir):
|
||||||
|
@ -49,8 +49,8 @@ def find_ap_randomizer_jar(forge_dir):
|
||||||
return None
|
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):
|
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:
|
if apmc_file is None:
|
||||||
return
|
return
|
||||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
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):
|
def read_apmc_file(apmc_file):
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
import json
|
|
||||||
|
|
||||||
with open(apmc_file, 'r') as f:
|
with open(apmc_file, 'r') as f:
|
||||||
data = json.loads(b64decode(f.read()))
|
return json.loads(b64decode(f.read()))
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# Check mod version, download new mod from GitHub releases page if needed.
|
def update_mod(forge_dir, minecraft_version: str, get_prereleases=False):
|
||||||
def update_mod(forge_dir, apmc_file, get_prereleases=False):
|
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
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"
|
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
|
||||||
resp = requests.get(client_releases_endpoint)
|
resp = requests.get(client_releases_endpoint)
|
||||||
if resp.status_code == 200: # OK
|
if resp.status_code == 200: # OK
|
||||||
try:
|
try:
|
||||||
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
|
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']),
|
(minecraft_version in release['assets'][0]['name']),
|
||||||
resp.json()))
|
resp.json()))
|
||||||
if ap_randomizer != latest_release['assets'][0]['name']:
|
if ap_randomizer != latest_release['assets'][0]['name']:
|
||||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
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)
|
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):
|
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")
|
eula_path = os.path.join(forge_dir, "eula.txt")
|
||||||
if not os.path.isfile(eula_path):
|
if not os.path.isfile(eula_path):
|
||||||
# Create eula.txt
|
# Create eula.txt
|
||||||
|
@ -152,17 +146,18 @@ def check_eula(forge_dir):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
# get the current JDK16
|
def find_jdk_dir(version: str) -> str:
|
||||||
def find_jdk_dir() -> str:
|
"""get the specified versions jdk directory"""
|
||||||
for entry in os.listdir():
|
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)
|
return os.path.abspath(entry)
|
||||||
|
|
||||||
|
|
||||||
# get the java exe location
|
def find_jdk(version: str) -> str:
|
||||||
def find_jdk() -> str:
|
"""get the java exe location"""
|
||||||
|
|
||||||
if is_windows:
|
if is_windows:
|
||||||
jdk = find_jdk_dir()
|
jdk = find_jdk_dir(version)
|
||||||
jdk_exe = os.path.join(jdk, "bin", "java.exe")
|
jdk_exe = os.path.join(jdk, "bin", "java.exe")
|
||||||
if os.path.isfile(jdk_exe):
|
if os.path.isfile(jdk_exe):
|
||||||
return jdk_exe
|
return jdk_exe
|
||||||
|
@ -173,16 +168,17 @@ def find_jdk() -> str:
|
||||||
return jdk_exe
|
return jdk_exe
|
||||||
|
|
||||||
|
|
||||||
# Download Corretto 16 (Amazon JDK)
|
def download_java(java: str):
|
||||||
def download_java():
|
"""Download Corretto (Amazon JDK)"""
|
||||||
jdk = find_jdk_dir()
|
|
||||||
|
jdk = find_jdk_dir(java)
|
||||||
if jdk is not None:
|
if jdk is not None:
|
||||||
print(f"Removing old JDK...")
|
print(f"Removing old JDK...")
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
rmtree(jdk)
|
rmtree(jdk)
|
||||||
|
|
||||||
print(f"Downloading Java...")
|
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)
|
resp = requests.get(jdk_url)
|
||||||
if resp.status_code == 200: # OK
|
if resp.status_code == 200: # OK
|
||||||
print(f"Extracting...")
|
print(f"Extracting...")
|
||||||
|
@ -197,9 +193,10 @@ def download_java():
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
# download and install forge
|
def install_forge(directory: str, forge_version: str, java_version: str):
|
||||||
def install_forge(directory: str):
|
"""download and install forge"""
|
||||||
jdk = find_jdk()
|
|
||||||
|
jdk = find_jdk(java_version)
|
||||||
if jdk is not None:
|
if jdk is not None:
|
||||||
print(f"Downloading Forge {forge_version}...")
|
print(f"Downloading Forge {forge_version}...")
|
||||||
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
|
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:
|
with open(forge_install_jar, 'wb') as f:
|
||||||
f.write(resp.content)
|
f.write(resp.content)
|
||||||
print(f"Installing Forge...")
|
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 = Popen(argstring, shell=not is_windows)
|
||||||
install_process.wait()
|
install_process.wait()
|
||||||
os.remove(forge_install_jar)
|
os.remove(forge_install_jar)
|
||||||
|
|
||||||
|
|
||||||
# Run the Forge server. Return process object
|
def run_forge_server(forge_dir: str, java_version: str, heap_arg: str) -> Popen:
|
||||||
def run_forge_server(forge_dir: str, heap_arg):
|
"""Run the Forge server."""
|
||||||
|
|
||||||
java_exe = find_jdk()
|
java_exe = find_jdk(java_version)
|
||||||
if not os.path.isfile(java_exe):
|
if not os.path.isfile(java_exe):
|
||||||
java_exe = "java" # try to fall back on java in the PATH
|
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']:
|
if heap_arg[-1] in ['b', 'B']:
|
||||||
heap_arg = heap_arg[:-1]
|
heap_arg = heap_arg[:-1]
|
||||||
heap_arg = "-Xmx" + heap_arg
|
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)
|
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__':
|
if __name__ == '__main__':
|
||||||
Utils.init_logging("MinecraftClient")
|
Utils.init_logging("MinecraftClient")
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
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',
|
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.")
|
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
|
||||||
parser.add_argument('--prerelease', default=False, action='store_true',
|
parser.add_argument('--release_channel', '-r', dest="channel", type=str, action='store',
|
||||||
help="Auto-update prerelease versions.")
|
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()
|
args = parser.parse_args()
|
||||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||||
|
|
||||||
# Change to executable's working directory
|
# Change to executable's working directory
|
||||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||||
|
|
||||||
options = Utils.get_options()
|
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"]
|
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
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 args.install:
|
||||||
if is_windows:
|
if is_windows:
|
||||||
print("Installing Java and Minecraft Forge")
|
print("Installing Java and Minecraft Forge")
|
||||||
download_java()
|
download_java(java_version)
|
||||||
else:
|
else:
|
||||||
print("Installing Minecraft Forge")
|
print("Installing Minecraft Forge")
|
||||||
install_forge(forge_dir)
|
install_forge(forge_dir, forge_version, java_version)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if apmc_file is not None and not os.path.isfile(apmc_file):
|
if apmc_data is None:
|
||||||
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
|
raise FileNotFoundError(f"APMC file does not exist or is inaccessible at the given location ({apmc_file})")
|
||||||
if not os.path.isdir(forge_dir):
|
|
||||||
if prompt_yes_no("Did not find forge directory. Download and install forge now?"):
|
if is_windows:
|
||||||
install_forge(forge_dir)
|
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):
|
if not os.path.isdir(forge_dir):
|
||||||
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
|
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
|
||||||
|
|
||||||
if not max_heap_re.match(max_heap):
|
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.")
|
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)
|
replace_apmc_files(forge_dir, apmc_file)
|
||||||
check_eula(forge_dir)
|
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()
|
server_process.wait()
|
||||||
|
|
3
Utils.py
3
Utils.py
|
@ -263,7 +263,8 @@ def get_default_options() -> dict:
|
||||||
},
|
},
|
||||||
"minecraft_options": {
|
"minecraft_options": {
|
||||||
"forge_directory": "Minecraft Forge server",
|
"forge_directory": "Minecraft Forge server",
|
||||||
"max_heap_size": "2G"
|
"max_heap_size": "2G",
|
||||||
|
"release_channel": "release"
|
||||||
},
|
},
|
||||||
"oot_options": {
|
"oot_options": {
|
||||||
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
||||||
|
|
|
@ -105,7 +105,10 @@ factorio_options:
|
||||||
minecraft_options:
|
minecraft_options:
|
||||||
forge_directory: "Minecraft Forge server"
|
forge_directory: "Minecraft Forge server"
|
||||||
max_heap_size: "2G"
|
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
|
# File name of the OoT v1.0 ROM
|
||||||
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
|
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
|
||||||
# Set this to false to never autostart a rom (such as after patching)
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
|
|
@ -14,8 +14,6 @@ from .Options import minecraft_options
|
||||||
from ..AutoWorld import World, WebWorld
|
from ..AutoWorld import World, WebWorld
|
||||||
|
|
||||||
client_version = 7
|
client_version = 7
|
||||||
minecraft_version = "1.17.1"
|
|
||||||
|
|
||||||
|
|
||||||
class MinecraftWebWorld(WebWorld):
|
class MinecraftWebWorld(WebWorld):
|
||||||
theme = "jungle"
|
theme = "jungle"
|
||||||
|
@ -47,7 +45,6 @@ class MinecraftWorld(World):
|
||||||
'player_name': self.world.get_player_name(self.player),
|
'player_name': self.world.get_player_name(self.player),
|
||||||
'player_id': self.player,
|
'player_id': self.player,
|
||||||
'client_version': client_version,
|
'client_version': client_version,
|
||||||
'minecraft_version': minecraft_version,
|
|
||||||
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits},
|
'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,
|
'advancement_goal': self.world.advancement_goal[self.player].value,
|
||||||
'egg_shards_required': min(self.world.egg_shards_required[self.player].value,
|
'egg_shards_required': min(self.world.egg_shards_required[self.player].value,
|
||||||
|
|
Loading…
Reference in New Issue