diff --git a/README.md b/README.md index 860499e..062c8c5 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ Original (Forge) Implementation by KonoTyran. ## Configuring your YAML file +### Unlockable Hearts + +This fork adds an `unlockable_hearts` option to the Minecraft APWorld. When enabled, players start with one heart, and nine progression `Heart` items are added to the item pool. Each received `Heart` restores one additional max heart up to the vanilla ten hearts. + +Use the packaged `minecraft.apworld` from this repository together with the matching NeoForgeAP mod jar built from this branch. + ### What is a YAML file and why do I need one? See the guide on setting up a basic YAML at the Archipelago setup diff --git a/apworld_src/minecraft/Constants.py b/apworld_src/minecraft/Constants.py new file mode 100644 index 0000000..6a296d9 --- /dev/null +++ b/apworld_src/minecraft/Constants.py @@ -0,0 +1,20 @@ +import json +import pkgutil + + +def load_data_file(*args) -> dict: + fname = "/".join(["data", *args]) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + + +item_info = load_data_file("items.json") +item_name_to_id = {name: index \ + for index, name in enumerate(item_info["all_items"], start=1)} + +location_info = load_data_file("locations.json") +location_name_to_id = {name: index \ + for index, name in enumerate(location_info["all_locations"], start=1)} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/apworld_src/minecraft/Container.py b/apworld_src/minecraft/Container.py new file mode 100644 index 0000000..2d8cc8b --- /dev/null +++ b/apworld_src/minecraft/Container.py @@ -0,0 +1,31 @@ +import json +import zipfile +from base64 import b64encode +from typing import Any, Optional + +from worlds.Files import APPlayerContainer + + +class MinecraftContainer(APPlayerContainer): + """ + Generates the apmc file + """ + game = "Minecraft" + patch_file_ending = ".apmc" + + def __init__(self, + patch_data: dict[str, Any], + patch_name: str, + path: Optional[str] = None, + player: Optional[int] = None, + player_name: str = "", + server: str = ""): + super().__init__(path, player, player_name, server) + self.patch_data = patch_data + self.patch_name = patch_name + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + super().write_contents(opened_zipfile) + filename = f"{self.patch_name}_json.apmcmeta" + opened_zipfile.writestr(filename,json.dumps(self.patch_data)) + diff --git a/apworld_src/minecraft/ItemPool.py b/apworld_src/minecraft/ItemPool.py new file mode 100644 index 0000000..874a89c --- /dev/null +++ b/apworld_src/minecraft/ItemPool.py @@ -0,0 +1,59 @@ +from math import ceil +from typing import List + +from BaseClasses import Item + +from . import Constants +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MinecraftWorld + + +def get_junk_item_names(rand, k: int) -> str: + junk_weights = Constants.item_info["junk_weights"] + junk = rand.choices( + list(junk_weights.keys()), + weights=list(junk_weights.values()), + k=k) + return junk + +def build_item_pool(world: "MinecraftWorld") -> List[Item]: + multiworld = world.multiworld + player = world.player + + itempool = [] + total_location_count = len(multiworld.get_unfilled_locations(player)) + + required_pool = Constants.item_info["required_pool"] + + # Add required progression items + for item_name, num in required_pool.items(): + itempool += [world.create_item(item_name) for _ in range(num)] + + # Add structure compasses + if world.options.structure_compasses: + compasses = [name for name in world.item_name_to_id if "Structure Compass" in name] + for item_name in compasses: + itempool.append(world.create_item(item_name)) + + # Dragon egg shards + if world.options.egg_shards_required > 0: + num = world.options.egg_shards_available + itempool += [world.create_item("Dragon Egg Shard") for _ in range(num)] + + # Unlockable hearts + if world.options.unlockable_hearts: + itempool += [world.create_item(f"Heart {heart}") for heart in range(1, 10)] + + # Bee traps + bee_trap_percentage = world.options.bee_traps * 0.01 + if bee_trap_percentage > 0: + bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool))) + itempool += [world.create_item("Bee Trap") for _ in range(bee_trap_qty)] + + # Fill remaining itempool with randomly generated junk + junk = get_junk_item_names(world.random, total_location_count - len(itempool)) + itempool += [world.create_item(name) for name in junk] + + return itempool diff --git a/apworld_src/minecraft/MinecraftClient.py b/apworld_src/minecraft/MinecraftClient.py new file mode 100644 index 0000000..ff8b5a5 --- /dev/null +++ b/apworld_src/minecraft/MinecraftClient.py @@ -0,0 +1,754 @@ +import argparse +import io +import json +import logging +import os +import pkgutil +import re +import shutil +import subprocess +import sys +import threading +import time +import zipfile +from base64 import b64encode, b64decode +from collections import defaultdict + +from enum import Enum +from math import floor, log +from queue import Queue +from typing import List, Optional, TYPE_CHECKING, Any, TypedDict + +import Utils + +from kivy import Config +from kivy.core.window import Window +from kivy.core.image import Image as CoreImage +from kivy.clock import Clock, mainthread +from kivy.lang import Builder +from kivy.properties import StringProperty, NumericProperty, ObjectProperty, ListProperty +from kivy.uix.popup import Popup +from kivy.uix.screenmanager import NoTransition +from kivy.uix.textinput import TextInput +from kivy.utils import escape_markup + +from kivymd.app import MDApp +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.gridlayout import MDGridLayout +from kivymd.uix.label import MDLabel +from kivymd.uix.recycleview import MDRecycleView +from kivymd.uix.screenmanager import MDScreenManager +from kivymd.uix.screen import MDScreen +from kivymd.uix.stacklayout import MDStackLayout +from kivymd.uix.widget import MDWidget + +from worlds.minecraft.downloader import StepsStep, SyncStep, BytesToStringStep +from worlds.minecraft.downloader.Java import DownloadJava +from worlds.minecraft.downloader.NeoForge import DownloadNeoForge +from worlds.minecraft.downloader.Utilities import DownloadStep, FetchStep + + +if TYPE_CHECKING: + from worlds.minecraft import MinecraftSettings + +logger = logging.getLogger("MinecraftClient") + +version_file_endpoint = "https://raw.githubusercontent.com/qixils/NeoForgeAP/refs/heads/main/versions/minecraft_versions.json" + +default_save_name = "Archipelago" + +class VersionsJson(TypedDict): + version: str + channel: str + data: int + java: int + minecraft: str + url: str + +def load_text(*path: str): + data_path = 'worlds.minecraft' if __name__ == '__main__' else __name__ + return pkgutil.get_data(data_path, "/".join(path)).decode() + + +def load_image(*path: str): + data_path = 'worlds.minecraft' if __name__ == '__main__' else __name__ + data = io.BytesIO(pkgutil.get_data(data_path, "/".join(path))) + texture = CoreImage(data, ext="png") + return texture + + +def format_bytes(size): + power = 0 if size <= 0 else floor(log(size, 1024)) + return f"{round(size / 1024 ** power, 2)} {['B', 'KB', 'MB', 'GB', 'TB'][int(power)]}" + + +def get_options() -> 'MinecraftSettings': + return Utils.get_settings()['minecraft_options'] + +class ServerStatus(Enum): + STOPPED = 0 + STARTING = 1 + RUNNING = 2 + + def __lt__(self, other): + return self.value < other.value + + def __gt__(self, other): + return self.value > other.value + + def __eq__(self, other): + return self.value == other.value + + +def get_recent_items(options: 'MinecraftSettings') -> List: + if not os.path.isdir(options.server_directory): + os.makedirs(options.server_directory) + saves = [] + for directory in os.listdir(options.server_directory): + try: + if directory.startswith("Archipelago-"): + world_dir = os.path.join(options.server_directory, directory) + + save = os.path.join(world_dir, "save.apmc") + if not os.path.isfile(save): + continue + + metadata_path = os.path.join(world_dir, "metadata.json") + if not os.path.exists(metadata_path): + # it doesn't really matter if the metadata exists... + # *except* it suggests the save.apmc is using the old broken format + # so we ignore this entry to force the user to re-import their apmc + continue + + description = default_save_name + with open(metadata_path, "r") as jsonfile: + metadata = json.load(jsonfile) + if 'description' in metadata: + description = metadata["description"] + saves.append((description, directory)) + except: + pass + return saves + + +class MinecraftClient(MDApp): + stop = threading.Event() + + def __init__(self, args, **kwargs): + super().__init__(**kwargs) + self.index = 0 + self.welcome_window: Optional[WelcomeWindow] = None + self.window_manager: Optional[WindowManager] = None + self.server_window: Optional[ServerWindow] = None + self.minecraft_versions: defaultdict[str, List[VersionsJson]] = defaultdict(lambda: list()) + # release channel to list of versions, I guess + self.apmc = None + self.version: VersionsJson = dict() + self.server = None + self.java_url = None + self.status: ServerStatus = ServerStatus.STOPPED + self.apmc_path = None + self.mod_info: List[VersionsJson] = list() + self.release_chanel = None + self.args = args + self.closing_at = 0 + Utils.init_logging('MinecraftClient') + logger.info(f"Client Initialized") + + # Handles (re)loading mod info, whether from a successful HTTP request or not + def _handle_mod_info(self, context: dict[str, Any], resp: Optional[Any] = None) -> None: + options = get_options() + fp = os.path.join(options.server_directory, 'ap-version.json') + logger.debug(f"Got response: {resp}") + if resp: + self.mod_info: List[VersionsJson] = json.loads(resp) + os.makedirs(options.server_directory, exist_ok=True) + with open(fp, 'w') as f: + json.dump(self.mod_info, f) + self._init_mc_versions() + return + + if not os.path.exists(fp): + return + + try: + with open(fp, 'r') as f: + self.mod_info: List[VersionsJson] = json.load(f) + self._init_mc_versions() + except: + logger.error("Failed to parse ap-version JSON", exc_info=True) + + + def _init_mc_versions(self): + self.minecraft_versions.clear() + for data in self.mod_info: + self.minecraft_versions[data['channel']].append(data) + + # Handles initializing the mod info fetching process + def _init_mod_info(self): + StepsStep( + "Download Versions", + SyncStep(self._handle_mod_info), # Load the cached JSON file + FetchStep(url=version_file_endpoint), # Download the latest version + BytesToStringStep(), + SyncStep(self._handle_mod_info), # Save the latest version (if available) + SyncStep(self.auto_start_server), + ).run(dict(), error_ok=True) + + def build(self): + logger.info(f"building client") + # TODO: Rewrite minecraft.kv to work with KivyMD. Look under the data file. + Builder.load_string(load_text("layouts", "minecraft.kv")) + self.window_manager = WindowManager(transition=NoTransition()) + self.welcome_window = WelcomeWindow(self) + self.server_window = ServerWindow(self) + self.window_manager.add_widget(self.welcome_window) + self.window_manager.add_widget(self.server_window) + logger.info(f"client built") + + logger.info(f"binding on close request") + Window.bind(on_request_close=self.on_request_close) + + logger.info(f"returning window manager") + return self.window_manager + + def on_start(self): + # send our request out to fetch the versions file + logger.info(f"fetching versions file") + self._init_mod_info() + + def on_request_close(self, *arg): + # if the server is stopped already then we don't need to intervene + if self.stop.is_set(): + return + + # likewise if there is no server, nothing to worry about + if not self.server: + return + + # if this is the first request to close the server, then let's send the shutdown signals + if self.closing_at == 0: + # save when shutdown started using increasing clock + # (`close` function's `dt` arg does not work like you might expect from the docs description; it is just a delta, so it isn't useful) + self.closing_at = time.monotonic() + + # send `/stop` to start server shutdown + self.send_command("stop") + + # check every tenth second for the server to be closed + Clock.schedule_interval(self.close, 0.1) + + # cancel the close request (we'll close it ourselves when we're ready) + return True + + + def close(self, dt): + # determine if X seconds have elapsed since starting + elapsed = time.monotonic() - self.closing_at + force_close = elapsed >= 15 + if force_close: + # server is not responding + try: + # attempt somewhat graceful shutdown + self.server.terminate() + self.server.wait(timeout=2) + except: + try: + # attempt less graceful shutdown + self.server.kill() + self.server.wait(timeout=2) + except Exception as e: + logger.warning("Failed to kill Minecraft server", exc_info=e) + pass + + # once server is stopped, close kivy window (has to be done manually since the close request was cancelled) + if self.stop.is_set() or force_close: + super().stop() + + def get_application_icon(self): + return load_image("assets", "icon.png") + + def init(self, dt=None): + options = get_options() + layout: MDWidget = self.welcome_window.ids.saves + layout.clear_widgets() + saves = get_recent_items(options) + if len(saves) == 0: + layout.add_widget(MDLabel(text="No saves")) + else: + for name, path in saves: + layout.add_widget(RecentItem(name=name, path=path, client=self)) + + ids: MDStackLayout = self.welcome_window.ids + ids.path.value = options.server_directory + ids.max_memory.value = options.max_heap_size + ids.min_memory.value = options.min_heap_size + ids.release_option.value = options.release_channel + ids.release_option.mc_options = self.minecraft_versions.keys() + + def auto_start_server(self, context: dict[str, Any], *ignore): + Clock.schedule_once(self.init, 1) + self.apmc_path = os.path.abspath(self.args.apmc_file) if type(self.args.apmc_file) is str else None + if self.apmc_path: + self.open_apmc(path=self.apmc_path) + + def read_possible_b64(self, data: str) -> dict[str, Any]: + if data.startswith("e"): + return json.loads(b64decode(data)) + elif data.startswith("{"): + return json.loads(data) + + def open_apmc(self, path=None): + options = get_options() + logger.info(self.mod_info) + self.apmc_path = path + if self.apmc_path is None: + self.apmc_path = Utils.open_filename(title="Choose AP Minecraft file", + filetypes=(("Archipelago Minecraft", ["*.apmc"]),)) + if self.apmc_path is None or self.apmc_path == "" or not os.path.isfile(self.apmc_path): + return + + # APContainer makes zips + try: + if zipfile.is_zipfile(self.apmc_path): + with zipfile.ZipFile(self.apmc_path, 'r') as zf: + embedded_apmc = None + for entry in zf.infolist(): + if entry.filename.endswith(".apmc") or entry.filename.endswith(".apmcmeta"): + embedded_apmc = entry.filename + break + if embedded_apmc: + with zf.open(embedded_apmc, 'r' ) as f: + data = f.read() + data = data.decode('utf-8') + apmc = self.read_possible_b64(data) + else: + logger.error(f"unable to find metadata file in zip") + else: + with open(self.apmc_path, 'r') as f: + data = f.read() + apmc = self.read_possible_b64(data) + except Exception as e: + logger.error("failed to load apmc file", exc_info=e) + + if apmc is None: + info_dialog("Invalid APMC", content=""" + An error occurred while parsing the APMC. + Please check the logs. + """) + return + + try: + self.apmc = apmc + # TODO(cang) Not sure if this is supposed to handle multiple valid MC versions + self.version = next(filter(lambda entry: entry['data'] == self.apmc["client_version"], + self.minecraft_versions[options.release_channel])) + self.server_window.status.text = f"Initializing {self.version['minecraft']}" + + self.window_manager.current = "Server" + self.start_server() + except KeyError: + logger.error(f"unable to find version {self.apmc['client_version']} on {options.release_channel}") + self.log_error(f"unable to find version {self.apmc['client_version']} on {options.release_channel}") + self.apmc_path = None + + def update_progress(self, value: float, content: str): + self.server_window.update_progress(value, content) + + def set_description(self, text): + self.apmc["description"] = text + self.start_server() + + @mainthread + def start_server(self) -> None: + options = get_options() + self.server_window.show_progress_bar_dialog("Installing Dependencies", "", 100) + context: dict[str, Any] = dict() + StepsStep( + "Download Dependencies", + DownloadJava(options.server_directory, self.version['java']), + DownloadNeoForge(options.server_directory, confirm_prompt, self.version["minecraft"]), + SyncStep(lambda context, data: (None, os.path.join(data.mods_dir, "Archipelago.jar"), self.version['version'])), + DownloadStep(self.version["url"]), + SyncStep(self.copy_apmc), + SyncStep(lambda *args: self.server_window.close_progress_bar_dialog()), + SyncStep(lambda *args: threading.Thread(target=self.server_thread, args=(context,)).start()) + ).run( + context, + on_failure=self.handle_server_start_failure, + on_progress=self.update_progress, + ) + + def handle_server_start_failure(self, *args): + logger.error(f"Dependency Downloads failed {args}", exc_info=True) + self.server_window.close_progress_bar_dialog() + self.window_manager.current = "Welcome" + info_dialog("Server Start Failure", content=""" + An error occurred while starting the server. + Please check the logs. + """) + + def copy_apmc(self, context: dict[str, Any], *arg): + if self.apmc_path is None: + return + neo_dir = context['neoforge_dir'] + apdata_dir = os.path.join(neo_dir, 'APData') + os.makedirs(apdata_dir, exist_ok=True) + + neo_apmc = os.path.join(apdata_dir, 'save.apmc') + shutil.copy2(self.apmc_path, neo_apmc) + + def server_thread(self, context: dict[str, Any]): + options = get_options() + + self.status = ServerStatus.STOPPED + self.server_window.background_color = (.5, .1, .1, 1) + world_name = f"Archipelago-{self.apmc['seed_name']}-P{self.apmc['player_id']}" + self.server_window.status.text = f"Archipelago-{self.apmc['seed_name']}-{self.apmc['player_name']}" + world_dir = os.path.join(options.server_directory, world_name) + if not os.path.isdir(world_dir): + os.makedirs(world_dir) + + save_path = os.path.join(world_dir, "save.apmc") + # we might consider skipping this if save_path exists + # but for now we want to repair broken installs + if self.apmc_path != save_path: + shutil.copy2(self.apmc_path, save_path) + + metadata_path = os.path.join(world_dir, "metadata.json") + if not os.path.isfile(metadata_path): + metadata = {"description": "Archipelago"} + with open(metadata_path, "w") as meta_file: + json.dump(metadata, meta_file) + + os.environ["JAVA_OPTS"] = "" + neo_run = context['neoforge_run_args'] + neo_dir = context['neoforge_dir'] + + cmd = (*neo_run, "--nogui", "--world", world_name) + logger.info(f"Invoking: {cmd}") + logger.info(f"With working dir: {neo_dir}") + self.server = subprocess.Popen(cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + encoding="utf-8", + text=True, + cwd=neo_dir, + ) + + server_queue = Queue() + stream_server_output(self.server.stdout, server_queue, self.server) + stream_server_output(self.server.stderr, server_queue, self.server) + + while not self.stop.is_set(): + if self.server.poll() is not None: + self.log_raw("[color=FFFF00]Minecraft server has exited.[/color]") + self.stop.set() + self.server_window.status.text = "Server Stopped" + self.server_window.background_color = (.5, .1, .1, 1) + self.status = ServerStatus.STOPPED + + while not server_queue.empty(): + raw_message: str = server_queue.get() + + match = re.match(r"^\[[0-9:]+] \[.+/(WARN|INFO|ERROR)] \[.+]: (.*)", raw_message) + if match: + level = match.group(1) + msg = escape_markup(match.group(2)) + + if level == "WARN": + self.log_warn(msg) + elif level == "ERROR": + self.log_error(msg) + elif level == "INFO": + self.log_info(msg) + else: + self.log_info(raw_message) + + if self.status < ServerStatus.RUNNING: + + server_starting_match = re.match(r"\[[0-9:]+] (?:\[[A-Za-z0-9 /]+] ?){1,2}: Starting minecraft server version ([0-9.]+)", raw_message) + if server_starting_match: + self.log_info(f"Starting Minecraft {server_starting_match.group(1)}") + self.server_window.status.text = f"Starting Server for {server_starting_match.group(1)}" + self.server_window.background_color = (.5, .5, .0, 1) + self.version["minecraft"] = server_starting_match.group(1) + self.status = ServerStatus.STARTING + + server_started_match = re.match(r"\[[0-9:]+] (?:\[[A-Za-z0-9 /]+] ?){1,2}: Done", raw_message) + if server_started_match: + self.server_window.status.text = f"Server Running. Connect to `127.0.0.1` in Minecraft {self.version['minecraft']}" + self.server_window.background_color = (.1, .5, .1, 1) + self.status = ServerStatus.RUNNING + + server_queue.task_done() + time.sleep(0.01) + + def send_command(self, cmd): + try: + self.server.stdin.write(f'{cmd}\n') + self.server.stdin.flush() + except Exception: + pass + + @mainthread + def log_info(self, msg): + self.server_window.log.on_message_markup(f"[b][INFO][/b] {escape_markup(msg)}") + + @mainthread + def log_warn(self, msg): + self.server_window.log.on_message_markup(f"[color=FFFF00][b][WARN][/b][/color] {escape_markup(msg)}") + + @mainthread + def log_error(self, msg): + self.server_window.log.on_message_markup(f"[color=FFFF00][b][ERROR][/b][/color] {escape_markup(msg)}") + + @mainthread + def log_raw(self, msg): + self.server_window.log.on_message_markup(msg) + + +def stream_server_output(pipe, queue, process): + def queuer(): + while process.poll() is None: + text = pipe.readline().rstrip().expandtabs() + if text: + queue.put_nowait(text) + + thread = threading.Thread(target=queuer, name="Minecraft Output Queue", daemon=True) + thread.start() + return thread + + +class TextOption(MDGridLayout): + value = StringProperty() + label = StringProperty() + button_label = StringProperty() + + +class DropdownOption(MDGridLayout): + value = StringProperty() + label = StringProperty() + options = ListProperty() + + +class FolderOption(TextOption): + + def button_press(self): + new_dir = Utils.open_directory(title="Choose Server Directory") + if new_dir: + self.value = new_dir + + +class RecentItem(MDBoxLayout): + name = StringProperty() + path = StringProperty() + client: MinecraftClient = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + icon_delete = load_image("assets", "delete.png") + icon_edit = load_image("assets", "edit.png") + self.ids.delete_icon.texture = icon_delete.texture + self.ids.rename_icon.texture = icon_edit.texture + + def load(self): + options = get_options() + save_path = os.path.join(options.server_directory, self.path, "save.apmc") + if os.path.isfile(save_path): + self.client.open_apmc(save_path) + else: + info_dialog(title="Error", content=f"Unable to find save file for world {self.path}") + + def delete(self): + self.client.welcome_window.confirm_delete(target=self.path, title="Confirm Delete", + content=f"Delete {self.name}?\nThis Action is permanent.") + + def rename(self): + edit_prompt(title="Confirm Edit", content=f"Rename {self.name}", default=self.name, + confirm=lambda text: self.set_name(text)) + + def set_name(self, name): + options = get_options() + self.name = name + try: + with open(os.path.join(options.server_directory, self.path, "metadata.json"), "r+") as file: + data = json.load(file) + data["description"] = name + file.seek(0) + file.truncate() + json.dump(data, file) + except Exception as e: + info_dialog(title="Error", content=f"Error renaming world: {e}") + + +class ConfirmDialog(Popup): + text = StringProperty() + confirm_text = StringProperty() + cancel_text = StringProperty() + + +class InfoDialog(Popup): + text = StringProperty() + button_text = StringProperty() + + +class ProgressBarDialog(Popup): + text = StringProperty("") + progress_text = StringProperty("") + progress = NumericProperty(0) + max = NumericProperty(100) + + def __init__(self, max, **kwargs): + super().__init__(**kwargs) + self.max = max + + +# TODO: migrate to Steps +def confirm_prompt(confirm=None, title="Prompt", content="Are you sure?", cancel=None, confirm_text="Yes", + cancel_text="No"): + popup = ConfirmDialog(title=title, text=content, confirm_text=confirm_text, cancel_text=cancel_text) + popup.open() + + if cancel is not None: + popup.ids.cancel.bind(on_press=cancel) + + if confirm is not None: + popup.ids.confirm.bind(on_press=confirm) + + +def info_dialog(title="Prompt", content="Are you sure?", cancel=None): + popup = InfoDialog(title=title, text=content, button_text="OK") + popup.open() + + +def edit_prompt(confirm, title="Prompt", content="Are you sure?", cancel=None, default=""): + popup = ConfirmDialog(title=title, text=content, confirm_text="Confirm", cancel_text="Cancel") + popup.open() + + content: MDWidget = popup.ids.content + + textinput = TextInput(text=default, + size_hint=(1, None), + height=30, + multiline=False, + ) + content.add_widget(textinput) + textinput.bind(on_text_validate=lambda _: confirm(textinput.text)) + textinput.bind(on_text_validate=popup.dismiss) + + if cancel is not None: + popup.ids.cancel.bind(on_press=cancel) + + popup.ids.confirm.bind(on_press=lambda _: confirm(textinput.text)) + + +class WindowManager(MDScreenManager): + pass + + +class LogEntry(MDLabel): + pass + + +class ServerWindow(MDScreen): + + def __init__(self, client, **kw): + super().__init__(**kw) + self.client = client + self.log: ServerLog = self.ids.log + self.status: MDLabel = self.ids.status + self.cmd: TextInput = self.ids.cmd + self.progress_popup: Optional[ProgressBarDialog] = None + self.background_color = (.5, .1, .1, 1) + + def send_command(self, value): + self.client.send_command(value) + self.cmd.text = "" + Clock.schedule_once(self.focus_cmd, 0) + + def focus_cmd(self, dv): + self.cmd.focus = True + + def show_progress_bar_dialog(self, title, content, max): + self.progress_popup = ProgressBarDialog(title=title, text=content, max=max) + self.progress_popup.open() + + def update_progress(self, progress: float, content: str): + if self.progress_popup is None: + return + self.progress_popup.progress = progress * 100.0 + self.progress_popup.progress_text = content + + def close_progress_bar_dialog(self): + if self.progress_popup is not None: + self.progress_popup.dismiss() + self.progress_popup = None + + +class ServerLog(MDRecycleView): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.data = [] + + def on_log(self, record: str): + self.data.append({"text": escape_markup(record)}) + self.clean_old() + + def on_message_markup(self, text): + self.data.append({"text": text}) + self.clean_old() + + def clean_old(self): + if len(self.data) > self.messages: + self.data.pop(0) + + +class WelcomeWindow(MDScreen): + version = StringProperty() + + def __init__(self, client: MinecraftClient, **kwargs): + super().__init__(**kwargs) + self.client = client + self.apmc = None + options = get_options() + Window.minimum_width, Window.minimum_height = (400, 300) + self.ids['path'].value = options.server_directory + self.ids['max_memory'].value = options.max_heap_size + self.ids['min_memory'].value = options.min_heap_size + self.ids['release_option'].value = options.release_channel + + def do_delete(self, target): + options = get_options() + world_path = os.path.join(options.server_directory, target) + if options.server_directory in world_path and os.path.isdir(world_path): + shutil.rmtree(world_path) + self.client.init() + + def confirm_delete(self, target, title="Confirm Delete", content="This Action is permanent."): + confirm_prompt(title=title, content=content, confirm=lambda _: self.do_delete(target)) + + def save_options(self): + options = get_options() + options.server_directory = self.ids.path.value + options.max_heap_size = self.ids.max_memory.value + options.min_heap_size = self.ids.min_memory.value + options.release_channel = self.ids.release_option.value + Utils.get_settings().save() + self.client.init() + +def launch_subprocess(*args): + from worlds.LauncherComponents import launch + launch(mc_launch, "Minecraft Client", args) + +def mc_launch(*arguments): + parser = argparse.ArgumentParser() + parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)") + args = parser.parse_args(arguments) + Config.set("network", "implementation", "requests") + MinecraftClient(args).run() + +if __name__ == "__main__": + mc_launch(sys.argv) \ No newline at end of file diff --git a/apworld_src/minecraft/Options.py b/apworld_src/minecraft/Options.py new file mode 100644 index 0000000..9a7afb4 --- /dev/null +++ b/apworld_src/minecraft/Options.py @@ -0,0 +1,149 @@ +from Options import Choice, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections, \ + PerGameCommonOptions +from .Constants import region_info +from dataclasses import dataclass + + +class AdvancementGoal(Range): + """Number of advancements required to spawn bosses.""" + display_name = "Advancement Goal" + range_start = 0 + range_end = 137 + default = 40 + + +class EggShardsRequired(Range): + """Number of dragon egg shards to collect to spawn bosses.""" + display_name = "Egg Shards Required" + range_start = 0 + range_end = 50 + default = 0 + + +class EggShardsAvailable(Range): + """Number of dragon egg shards available to collect.""" + display_name = "Egg Shards Available" + range_start = 0 + range_end = 50 + default = 0 + + +class BossGoal(Choice): + """Bosses which must be defeated to finish the game.""" + display_name = "Required Bosses" + option_none = 0 + option_ender_dragon = 1 + option_wither = 2 + option_both = 3 + default = 1 + + @property + def dragon(self): + return self.value % 2 == 1 + + @property + def wither(self): + return self.value > 1 + + +class ShuffleStructures(DefaultOnToggle): + """Enables shuffling of villages, outposts, fortresses, bastions, and end cities.""" + display_name = "Shuffle Structures" + + +class StructureCompasses(DefaultOnToggle): + """Adds structure compasses to the item pool, which point to the nearest indicated structure.""" + display_name = "Structure Compasses" + + +class BeeTraps(Range): + """Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when + received.""" + display_name = "Bee Trap Percentage" + range_start = 0 + range_end = 100 + default = 0 + + +class CombatDifficulty(Choice): + """Modifies the level of items logically required for exploring dangerous areas and fighting bosses.""" + display_name = "Combat Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 + + +class HardAdvancements(Toggle): + """Enables certain RNG-reliant or tedious advancements.""" + display_name = "Include Hard Advancements" + + +class UnreasonableAdvancements(Toggle): + """Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\"""" + display_name = "Include Unreasonable Advancements" + + +class PostgameAdvancements(Toggle): + """Enables advancements that require spawning and defeating the required bosses.""" + display_name = "Include Postgame Advancements" + + +class SendDefeatedMobs(Toggle): + """Send killed mobs to other Minecraft worlds which have this option enabled.""" + display_name = "Send Defeated Mobs" + + +class UnlockableHearts(Toggle): + """Start players with one heart and add nine Heart items to the item pool. Each Heart restores one max heart, up to the vanilla ten hearts.""" + display_name = "Unlockable Hearts" + + +class StartingItems(OptionList): + """Start with these items. Each entry should be of this format: {item: "item_name", amount: #} + `item` can include components, and should be in an identical format to a `/give` command with + `"` escaped for json reasons. + + `amount` is optional and will default to 1 if omitted. + + example: + ``` + starting_items: [ + { "item": "minecraft:stick[minecraft:custom_name=\"{'text':'pointy stick'}\"]" }, + { "item": "minecraft:arrow[minecraft:rarity=epic]", amount: 64 } + ] + ``` + """ + display_name = "Starting Items" + + +class MCPlandoConnections(PlandoConnections): + entrances = set(connection[0] for connection in region_info["default_connections"]) + exits = set(connection[1] for connection in region_info["default_connections"]) + + @classmethod + def can_connect(cls, entrance, exit): + if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]: + return False + return True + + +@dataclass +class MinecraftOptions(PerGameCommonOptions): + plando_connections: MCPlandoConnections + advancement_goal: AdvancementGoal + egg_shards_required: EggShardsRequired + egg_shards_available: EggShardsAvailable + required_bosses: BossGoal + shuffle_structures: ShuffleStructures + structure_compasses: StructureCompasses + + combat_difficulty: CombatDifficulty + include_hard_advancements: HardAdvancements + include_unreasonable_advancements: UnreasonableAdvancements + include_postgame_advancements: PostgameAdvancements + bee_traps: BeeTraps + send_defeated_mobs: SendDefeatedMobs + unlockable_hearts: UnlockableHearts + death_link: DeathLink + starting_items: StartingItems diff --git a/apworld_src/minecraft/Rules.py b/apworld_src/minecraft/Rules.py new file mode 100644 index 0000000..e1a83c9 --- /dev/null +++ b/apworld_src/minecraft/Rules.py @@ -0,0 +1,690 @@ +from BaseClasses import CollectionState +from worlds.generic.Rules import exclusion_rules + +from . import Constants +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MinecraftWorld + + +# Helper functions +# moved from logicmixin + +def has_iron_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + + +def has_copper_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + + +def has_gold_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.has('Progressive Resource Crafting', player) + and ( + state.has('Progressive Tools', player, 2) + or state.can_reach_region('The Nether', player) + ) + ) + + +def has_diamond_pickaxe(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player, 3) and has_iron_ingots(world, state, player) + + +def craft_crossbow(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Archery', player) and has_iron_ingots(world, state, player) + + +def has_bottle(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player) + + +def has_spyglass(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (has_copper_ingots(world, state, player) + and state.has('Spyglass', player) + and can_adventure(world, state, player) + ) + + +def can_enchant(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and has_diamond_pickaxe(world, state, player) # mine obsidian and lapis + + +def can_use_anvil(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.has('Enchanting', player) + and state.has('Progressive Resource Crafting', player,2) + and has_iron_ingots(world, state, player) + ) + + +def fortress_loot(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: # blaze rods, wither skulls + return state.can_reach_region('Nether Fortress', player) and basic_combat(world, state, player) + + +def can_excavate(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (has_copper_ingots(world, state, player) and state.has('Brush', player) + and can_adventure(world, state, player) + ) + + +def can_brew_potions(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(world, state, player) + +def can_piglin_trade(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (has_gold_ingots(world, state, player) + and ( + state.can_reach_region('The Nether', player) + or state.can_reach_region('Bastion Remnant', player) + )) + + +def overworld_villager(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name + if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village + return (state.can_reach_location('Zombie Doctor', player) + or ( + has_diamond_pickaxe(world, state, player) + and state.can_reach_region('Village', player) + )) + elif village_region == 'The End': + return state.can_reach_location('Zombie Doctor', player) + return state.can_reach_region('Village', player) or state.can_reach_location('Zombie Doctor', player) + + +def enter_stronghold(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player) + + +# Difficulty-dependent functions +def combat_difficulty(world: "MinecraftWorld", state: CollectionState, player: int) -> str: + return world.options.combat_difficulty.current_key + + +def can_adventure(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + death_link_check = not world.options.death_link or state.has('Bed', player) + if combat_difficulty(world, state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and has_iron_ingots(world, state, player) and death_link_check + elif combat_difficulty(world, state, player) == 'hard': + return True + return (state.has('Progressive Weapons', player) and death_link_check and + (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) + + +def basic_combat(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + if combat_difficulty(world, state, player) == 'easy': + return (state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + and state.has('Shield', player) + and has_iron_ingots(world, state, player) + ) + elif combat_difficulty(world, state, player) == 'hard': + return True + return (state.has('Progressive Weapons', player) + and ( + state.has('Progressive Armor', player) + or state.has('Shield', player) + ) + and has_iron_ingots(world, state, player) + ) + + +def ominous_vaults(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + if combat_difficulty(world, state, player) == 'easy': + return (state.can_reach_region("Pillager Outpost", player) + and state.has('Progressive Weapons', player, 3) + and state.has('Progressive Armor', player, 2) + and state.has('Shield', player) + and state.has('Progressive Tools', player, 2) + and has_iron_ingots(world, state, player) + ) + elif combat_difficulty(world, state, player) == 'hard': + return (state.can_reach_region("Pillager Outpost", player) + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and ( + state.has('Progressive Armor', player) + or state.has('Shield', player) + ) + ) + return (state.can_reach_region("Pillager Outpost", player) + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and state.has('Progressive Armor', player) + and state.has('Shield', player) + ) + + +def complete_raid(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + reach_regions = (state.can_reach_region('Village', player) + and state.can_reach_region('Pillager Outpost', player)) + if combat_difficulty(world, state, player) == 'easy': + return (reach_regions + and state.has('Progressive Weapons', player, 3) + and state.has('Progressive Armor', player, 2) + and state.has('Shield', player) + and state.has('Archery', player) + and state.has('Progressive Tools', player, 2) + and has_iron_ingots(world, state, player) + ) + elif combat_difficulty(world, state, player) == 'hard': # might be too hard? + return (reach_regions + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and ( + state.has('Progressive Armor', player) + or state.has('Shield', player) + ) + ) + return (reach_regions + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and state.has('Progressive Armor', player) + and state.has('Shield', player) + ) + + +def can_kill_wither(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + normal_kill = (state.has("Progressive Weapons", player, 3) + and state.has("Progressive Armor", player, 2) + and can_brew_potions(world, state, player) + and can_enchant(world, state, player) + ) + if combat_difficulty(world, state, player) == 'easy': + return (fortress_loot(world, state, player) + and normal_kill + and state.has('Archery', player) + ) + elif combat_difficulty(world, state, player) == 'hard': # cheese kill using bedrock ceilings + return (fortress_loot(world, state, player) + and ( + normal_kill + or state.can_reach_region('The Nether', player) + or state.can_reach_region('The End', player) + ) + ) + + return fortress_loot(world, state, player) and normal_kill + + +def can_respawn_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.can_reach_region('The Nether', player) + and state.can_reach_region('The End', player) + and state.has('Progressive Resource Crafting', player) # smelt sand into glass + ) + + +def can_kill_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + if combat_difficulty(world, state, player) == 'easy': + return (state.has("Progressive Weapons", player, 3) + and state.has("Progressive Armor", player, 2) + and state.has('Archery', player) + and can_brew_potions(world, state, player) + and can_enchant(world, state, player) + ) + if combat_difficulty(world, state, player) == 'hard': + return ( + ( + state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + ) or ( + state.has('Progressive Weapons', player, 1) + and state.has('Bed', player) # who needs armor when you can respawn right outside the chamber + ) + ) + return (state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + and state.has('Archery', player) + ) + + +def has_structure_compass(world: "MinecraftWorld", state: CollectionState, entrance_name: str, player: int) -> bool: + if not world.options.structure_compasses: + return True + return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) + + +def get_rules_lookup(world, player: int): + rules_lookup = { + "entrances": { + "Nether Portal": lambda state: state.has('Flint and Steel', player) + and ( + state.has('Bucket', player) + or state.has('Progressive Tools', player, 3) + ) + and has_iron_ingots(world, state, player), + "End Portal": lambda state: enter_stronghold(world, state, player) + and state.has('3 Ender Pearls', player, 4), + "Overworld Structure 1": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Overworld Structure 1", player), + "Overworld Structure 2": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Overworld Structure 2", player), + "Nether Structure 1": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Nether Structure 1", player), + "Nether Structure 2": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Nether Structure 2", player), + "The End Structure": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "The End Structure", player), + "Ocean": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Ocean", player), + "Dark Forest": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Dark Forest", player), + "Deep Dark": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2) + and has_structure_compass(world, state, "Deep Dark", player), + "Ruins": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Ruins", player), + "Underground": lambda state: can_adventure(world, state, player) and state.has("Progressive Tools", player) + and has_structure_compass(world, state, "Underground", player) + }, + "locations": { + "Ender Dragon": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "Wither": lambda state: can_kill_wither(world, state, player), + "Blaze Rods": lambda state: fortress_loot(world, state, player), + "Who is Cutting Onions?": lambda state: can_piglin_trade(world, state, player), + "Oh Shiny": lambda state: can_piglin_trade(world, state, player), + "Suit Up": lambda state: state.has("Progressive Armor", player) + and has_iron_ingots(world, state, player), + "Very Very Frightening": lambda state: state.has("Channeling Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + and overworld_villager(world, state, player), + "Hot Stuff": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "Free the End": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "A Furious Cocktail": lambda state: (can_brew_potions(world, state, player) + and state.has("Fishing Rod", player) # Water Breathing + and state.can_reach_region("The Nether", player) # Regeneration, Fire Resistance, gold nuggets + and state.can_reach_region("Village", player) # Night Vision, Invisibility + and state.can_reach_location("Bring Home the Beacon", player) # Resistance + and can_adventure(world, state, player) + and state.can_reach_region("Trial Chambers", player) # Wind Charged + ), + "Bring Home the Beacon": lambda state: can_kill_wither(world, state, player) + and has_diamond_pickaxe(world, state, player) + and state.has("Progressive Resource Crafting", player, 2), + "Not Today, Thank You": lambda state: state.has("Shield", player) + and has_iron_ingots(world, state, player), + "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Local Brewery": lambda state: can_brew_potions(world, state, player), + "The Next Generation": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "Fishy Business": lambda state: state.has("Fishing Rod", player), + "This Boat Has Legs": lambda state: has_iron_ingots(world, state, player) + and state.has("Saddle", player) + and state.has("Fishing Rod", player), + "Sniper Duel": lambda state: state.has("Archery", player), + "Great View From Up Here": lambda state: basic_combat(world, state, player), + "How Did We Get Here?": lambda state: (can_brew_potions(world, state, player) + and has_gold_ingots(world, state, player) # Absorption + and state.can_reach_region('End City', player) # Levitation + and state.can_reach_region('The Nether', player) # potion ingredients + and state.can_reach_region('Ocean Monument', player) # Heart of the Sea, Dolphin's Grace, Mining Fatigue + and state.can_reach_region('Ancient City', player) # Darkness + and state.can_reach_region('Trial Chambers', player) # Wind Charged + and state.has("Fishing Rod", player) # Pufferfish, Nautilus Shells + and state.has("Archery", player) # Spectral Arrows + and state.can_reach_location("Bring Home the Beacon", player) # Haste + and state.can_reach_location("Hero of the Village", player)), # Bad Omen, Hero of the Village + "Bullseye": lambda state: state.has("Archery", player) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Spooky Scary Skeleton": lambda state: basic_combat(world, state, player), + "Two by Two": lambda state: can_excavate(world, state, player) + and state.can_reach_region("Ocean Monument", player) # Sniffers + and state.has("Bucket", player) # Axolotls + and state.can_reach_region("Village", player) # Cats + and state.has("Brush", player) + and state.has("Fishing Rod", player), # Pufferfish for Nautiluses + "Two Birds, One Arrow": lambda state: craft_crossbow(world, state, player) + and can_enchant(world, state, player), + "Who's the Pillager Now?": lambda state: craft_crossbow(world, state, player), + "Getting an Upgrade": lambda state: state.has("Progressive Tools", player), + "Tactical Fishing": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "Zombie Doctor": lambda state: can_brew_potions(world, state, player) + and has_gold_ingots(world, state, player), + "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(world, state, player), + "Into Fire": lambda state: basic_combat(world, state, player), + "War Pigs": lambda state: basic_combat(world, state, player), + "Take Aim": lambda state: state.has("Archery", player), + "Total Beelocation": lambda state: state.has("Silk Touch Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player), + "Arbalistic": lambda state: (craft_crossbow(world, state, player) + and state.has("Piercing IV Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + ), + "The End... Again...": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "Acquire Hardware": lambda state: has_iron_ingots(world, state, player), + "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(world, state, player) + and state.has("Progressive Resource Crafting", player, 2), + "Cover Me with Diamonds": lambda state: state.has("Progressive Armor", player, 2) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Sky's the Limit": lambda state: basic_combat(world, state, player), + "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) + and has_iron_ingots(world, state, player), + "Sweet Dreams": lambda state: state.has("Bed", player) + or state.can_reach_region('Village', player), + "You Need a Mint": lambda state: can_respawn_ender_dragon(world, state, player) + and has_bottle(world, state, player), + "Monsters Hunted": lambda state: can_respawn_ender_dragon(world, state, player) # Ghast, Hoglin, Magma Cube, Piglin + and can_kill_ender_dragon(world, state, player) # Ender Dragon, Enderman, Endermite, Silverfish + and can_kill_wither(world, state, player) # Blaze, Wither, Wither Skeleton, Zombified Piglin + and complete_raid(world, state, player) # Ravagers; Pillager Outposts + and state.can_reach_region('Bastion Remnant', player) # Piglin Brute + and state.can_reach_region('End City', player) # Shulker + and state.can_reach_region('Trial Chambers', player) # Breeze + and state.has("Lead", player) # Zoglins + and state.can_reach_region('Ocean Monument', player) # Drowned + and ( + (can_brew_potions(world, state, player) and state.has("Fishing Rod", player)) # Water Breathing Potions for Elder Guardian, Guardian + or (can_enchant(world, state, player) and state.has("Bucket", player)) # Aqua Affinity/Respiration and Milk/Axolotls for Elder Guardian, Guardian + ), + "Enchanter": lambda state: can_enchant(world, state, player), + "Voluntary Exile": lambda state: basic_combat(world, state, player), + "Eye Spy": lambda state: enter_stronghold(world, state, player), + "Serious Dedication": lambda state: (state.can_reach_location("Hidden in the Depths", player) + and state.has("8 Netherite Scrap", player) + and has_gold_ingots(world, state, player)), + "Postmortal": lambda state: complete_raid(world, state, player), + "Adventuring Time": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2) + and state.can_reach_region('Ocean Monument', player) # Most Oceans + and state.can_reach_region('Woodland Mansion', player) # Dark Forest + and state.can_reach_region('Ancient City', player) # Deep Dark + and state.can_reach_region('Trail Ruins', player), # Jungle, Birch Forest, Old Growth Birch Forest, all Taiga variants + "Hero of the Village": lambda state: complete_raid(world, state, player), + "Hidden in the Depths": lambda state: can_brew_potions(world, state, player) + and state.has("Bed", player) + and has_diamond_pickaxe(world, state, player), + "Beaconator": lambda state: (can_kill_wither(world, state, player) + and has_diamond_pickaxe(world, state, player) + and state.has("Progressive Resource Crafting", player, 2)), + "Withering Heights": lambda state: can_kill_wither(world, state, player), + "A Balanced Diet": lambda state: (has_bottle(world, state, player) # honey bottle + and state.has("Campfire", player) # honey bottle + and state.has("Fishing Rod", player) + and state.can_reach_location("Overpowered", player) # gapple, notch apple + and state.can_reach_region('The End', player)), # chorus fruit + "Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player), + "Country Lode, Take Me Home": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Bee Our Guest": lambda state: state.has("Campfire", player) + and has_bottle(world, state, player), + "Uneasy Alliance": lambda state: has_diamond_pickaxe(world, state, player) + and state.has('Fishing Rod', player), + "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "A Throwaway Joke": lambda state: basic_combat(world, state, player), + "Sticky Situation": lambda state: state.has("Campfire", player) + and has_bottle(world, state, player), + "Ol' Betsy": lambda state: craft_crossbow(world, state, player), + "Cover Me in Debris": lambda state: state.has("Progressive Armor", player, 2) + and state.has("8 Netherite Scrap", player, 2) + and state.can_reach_location("Hidden in the Depths", player), + "Hot Topic": lambda state: state.has("Progressive Resource Crafting", player), + "The Lie": lambda state: has_iron_ingots(world, state, player) + and state.has("Bucket", player), + "On a Rail": lambda state: has_iron_ingots(world, state, player) + and state.has('Progressive Tools', player, 2), + "When Pigs Fly": lambda state: has_iron_ingots(world, state, player) + and state.has("Saddle", player) + and state.has("Fishing Rod", player) + and can_adventure(world, state, player), + "Overkill": lambda state: ( + can_brew_potions(world, state, player) + and ( + state.has("Progressive Weapons", player) + or state.can_reach_region('The Nether', player) + ) + ) + or ( + state.can_reach_location("Over-Overkill", player) + and world.options.include_hard_advancements + and "Over-Overkill" not in world.options.exclude_locations.value + ), + "Librarian": lambda state: state.has("Enchanting", player), + "Overpowered": lambda state: has_iron_ingots(world, state, player) + and state.has('Progressive Tools', player, 2) + and basic_combat(world, state, player), + "Wax On": lambda state: state.has('Campfire', player) + and has_copper_ingots(world, state, player), + "Wax Off": lambda state: ( + has_copper_ingots(world, state, player) + and state.has('Campfire', player) + ) + or state.can_reach_region("Trial Chambers", player), + "The Cutest Predator": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "The Healing Power of Friendship": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "Is It a Bird?": lambda state: has_spyglass(world, state, player), + "Is It a Balloon?": lambda state: has_spyglass(world, state, player), + "Is It a Plane?": lambda state: has_spyglass(world, state, player) + and can_respawn_ender_dragon(world, state, player), + "Surge Protector": lambda state: state.has("Channeling Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + and overworld_villager(world, state, player), + "Light as a Rabbit": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "Glow and Behold!": lambda state: can_adventure(world, state, player), + "Whatever Floats Your Goat!": lambda state: can_adventure(world, state, player), + "Caves & Cliffs": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and state.has('Progressive Tools', player, 2), + "Feels Like Home": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and state.has('Fishing Rod', player) + and state.has("Saddle", player), + "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player) + and can_adventure(world, state, player) + and ( + basic_combat(world, state, player) + or state.can_reach_region("The Nether", player) + or state.can_reach_region("Ancient City", player) + ), + "Star Trader": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and ( + state.can_reach_region("The Nether", player) # soul sand in nether + or state.can_reach_region("Nether Fortress", player) # soul sand in fortress if not in nether for water elevator + or can_piglin_trade(world, state, player) # piglins give soul sand + ) + and overworld_villager(world, state, player), + "Birthday Song": lambda state: state.can_reach_location("The Lie", player) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player) + and ( + state.can_reach_region('Pillager Outpost', player) + or ( + basic_combat(world, state, player) + and state.can_reach_region('Woodland Mansion', player) + ) + ), + "Bukkit Bukkit": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player) + and can_adventure(world, state, player), + "It Spreads": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2), + "Sneak 100": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2), + "When the Squad Hops into Town": lambda state: can_adventure(world, state, player) + and state.has("Lead", player) + and state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "With Our Powers Combined!": lambda state: can_adventure(world, state, player) + and state.has("Lead", player) + and state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "You've Got a Friend in Me": lambda state: state.can_reach_region('Pillager Outpost', player) + or ( + basic_combat(world, state, player) + and state.can_reach_region('Woodland Mansion', player) + ), + "Smells Interesting": lambda state: can_excavate(world, state, player), + "Little Sniffs": lambda state: can_excavate(world, state, player), + "Planting the Past": lambda state: can_excavate(world, state, player), + "Crafting a New Look": lambda state: has_iron_ingots(world, state, player) # Maybe streamline this one + and ( + fortress_loot(world, state, player) + or ( + state.can_reach_region("Pillager Outpost", player) + and basic_combat(world, state, player) + ) + or ( + state.can_reach_region("Bastion Remnant", player) + and basic_combat(world, state, player) + ) + or ( + state.can_reach_region("End City", player) + and basic_combat(world, state, player) + ) + or ( + state.can_reach_region("Ocean Monument", player) + and basic_combat(world, state, player) + and state.has("Bucket", player) + and can_enchant(world, state, player) + ) + or ( + state.can_reach_region("Woodland Mansion", player) + and basic_combat(world, state, player) + ) + or state.can_reach_region("Ancient City", player) + or ( + state.can_reach_region("Trail Ruins", player) + and state.has("Brush", player) + ) + ), + "Smithing with Style": lambda state: can_excavate(world, state, player) # Wayfinder Armor Trim + and fortress_loot(world, state, player) # Rib Armor Trim + and state.can_reach_region("Bastion Remnant", player) # Snout Armor Trim + and state.can_reach_region("End City", player) # Spire Armor Trim + and ( + ( # Water Breathing Potions + state.has("Fishing Rod", player) + and can_brew_potions(world, state, player) + ) + or ( + state.has("Bucket", player) # Milk/Axolotls + and can_enchant(world, state, player) # Respiration + ) + ) + and state.can_reach_region("Woodland Mansion", player) # Vex Armor Trim + and state.can_reach_region("Ancient City", player) # Ward and Silence Armor Trims + and state.can_reach_region("Trail Ruins", player) + and state.can_reach_region("Ocean Monument", player), # Tide Armor Trim + "Respecting the Remnants": lambda state: can_excavate(world, state, player) + and ( + state.can_reach_region("Ocean Monument", player) + or state.can_reach_region("Trail Ruins", player) + ), + "Careful Restoration": lambda state: can_excavate(world, state, player) + and ( + state.can_reach_region("Ocean Monument", player) + or state.can_reach_region("Trail Ruins", player) + ), + "The Power of Books": lambda state: state.has("Progressive Tools", player, 2), + "Isn't It Scute?": lambda state: can_adventure(world, state, player) + and has_copper_ingots(world, state, player) + and state.has("Brush", player), + "Shear Brilliance": lambda state: can_adventure(world, state, player) + and has_copper_ingots(world, state, player) + and state.has("Brush", player), + "Good as New": lambda state: can_adventure(world, state, player) + and has_copper_ingots(world, state, player) + and state.has("Brush", player), + "The Whole Pack": lambda state: can_adventure(world, state, player), + "Under Lock and Key": lambda state: basic_combat(world, state, player), + "Blowback": lambda state: basic_combat(world, state, player), + "Who Needs Rockets?": lambda state: basic_combat(world, state, player), + "Crafters Crafting Crafters": lambda state: has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2), + "Lighten Up": lambda state: ( + fortress_loot(world, state, player) + and state.has("Progressive Tools", player, 2) + and state.has("Progressive Resource Crafting", player, 2) + ) + or state.can_reach_region("Trial Chambers", player), + "Over-Overkill": lambda state: ominous_vaults(world, state, player), + "Revaulting": lambda state: ominous_vaults(world, state, player), + "Stay Hydrated!": lambda state: state.can_reach_region("The Nether", player) + or can_piglin_trade(world, state, player), + "Heart Transplanter": lambda state: can_adventure(world, state, player) + and ( + ( + basic_combat(world, state, player) + and state.has("Progressive Resource Crafting", player, 2) + ) + or ( + state.has("Silk Touch Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + ) + ), + "Mob Kabob": lambda state: state.has("Progressive Resource Crafting", player) + } + } + return rules_lookup + + +def set_rules(self: "MinecraftWorld") -> None: + multiworld = self.multiworld + player = self.player + + rules_lookup = get_rules_lookup(self, player) + + # Set entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set location rules + for location_name, rule in rules_lookup["locations"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set rules surrounding completion + bosses = self.options.required_bosses + postgame_advancements = set() + if bosses.dragon: + postgame_advancements.update(Constants.exclusion_info["ender_dragon"]) + if bosses.wither: + postgame_advancements.update(Constants.exclusion_info["wither"]) + + def location_count(state: CollectionState) -> int: + return len([location for location in multiworld.get_locations(player) if + location.address is not None and + location.can_reach(state)]) + + def defeated_bosses(state: CollectionState) -> bool: + return ((not bosses.dragon or state.has("Ender Dragon", player)) + and (not bosses.wither or state.has("Wither", player))) + + egg_shards = min(self.options.egg_shards_required.value, self.options.egg_shards_available.value) + completion_requirements = lambda state: (location_count(state) >= self.options.advancement_goal + and state.has("Dragon Egg Shard", player, egg_shards)) + multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state) + + # Set exclusions on hard/unreasonable/postgame + excluded_advancements = set() + if not self.options.include_hard_advancements: + excluded_advancements.update(Constants.exclusion_info["hard"]) + if not self.options.include_unreasonable_advancements: + excluded_advancements.update(Constants.exclusion_info["unreasonable"]) + if not self.options.include_postgame_advancements: + excluded_advancements.update(postgame_advancements) + exclusion_rules(multiworld, player, excluded_advancements) diff --git a/apworld_src/minecraft/Structures.py b/apworld_src/minecraft/Structures.py new file mode 100644 index 0000000..630dac1 --- /dev/null +++ b/apworld_src/minecraft/Structures.py @@ -0,0 +1,67 @@ +from . import Constants +from typing import TYPE_CHECKING +from Options import PlandoConnection +if TYPE_CHECKING: + from . import MinecraftWorld + + +def shuffle_structures(self: "MinecraftWorld") -> None: + multiworld = self.multiworld + player = self.player + + default_connections = Constants.region_info["default_connections"] + illegal_connections = Constants.region_info["illegal_connections"] + + # Get all unpaired exits and all regions without entrances (except the Menu) + # This function is destructive on these lists. + exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region is None] + structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] + exits_spoiler = exits[:] # copy the original order for the spoiler log + + pairs = {} + + def set_pair(exit, struct): + if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): + pairs[exit] = struct + exits.remove(exit) + structs.remove(struct) + else: + raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})") + + if self.using_ut: + self.options.plando_connections.value.clear() + for exit, struct in self.passthrough["structures"].items(): + exit_name = exit + struct_name = struct + self.options.plando_connections.value.append(PlandoConnection(exit_name, struct_name, "both")) + + # Connect plando structures first + if self.options.plando_connections: + for conn in self.options.plando_connections: + set_pair(conn.entrance, conn.exit) + + # The algorithm tries to place the most restrictive structures first. This algorithm always works on the + # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. + if self.options.shuffle_structures: + structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) + for struct in structs[:]: + try: + exit = self.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) + except IndexError: + raise Exception(f"No valid structure placements remaining for player {player} ({self.player_name})") + set_pair(exit, struct) + else: # write remaining default connections + for (exit, struct) in default_connections: + if exit in exits: + set_pair(exit, struct) + + # Make sure we actually paired everything; might fail if plando + try: + assert len(exits) == len(structs) == 0 + except AssertionError: + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({self.player_name})") + + for exit in exits_spoiler: + multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player)) + if self.options.shuffle_structures or self.options.plando_connections: + multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) diff --git a/apworld_src/minecraft/__init__.py b/apworld_src/minecraft/__init__.py new file mode 100644 index 0000000..7a05e2c --- /dev/null +++ b/apworld_src/minecraft/__init__.py @@ -0,0 +1,279 @@ +import os +import json +import settings +import typing +from base64 import b64encode, b64decode +from typing import Dict, Any + +from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Location +from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier + +from . import Constants +from .Container import MinecraftContainer +from .Options import MinecraftOptions +from .Structures import shuffle_structures +from .ItemPool import build_item_pool, get_junk_item_names +from .Rules import set_rules +from ..LauncherComponents import icon_paths + +client_version = 10 + +icon_paths['mcicon'] = f"ap:{__name__}/assets/mcicon.png" + +# register client +def launch_client(*args): + from .MinecraftClient import launch_subprocess + launch_subprocess(*args) + +components.append( + Component( + "Minecraft Client", + icon="mcicon", + func=launch_client, + component_type=Type.CLIENT, + file_identifier=SuffixIdentifier('.apmc'), + ) +) + + +class MinecraftSettings(settings.Group): + class ForgeDirectory(settings.OptionalUserFolderPath): + pass + + class ReleaseChannel(str): + """ + 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. + """ + + class JavaExecutable(settings.OptionalUserFilePath): + """ + Path to Java executable. If not set, will attempt to fall back to Java system installation. + """ + + class ServerDirectory(settings.OptionalUserFolderPath): + """ + Path to local directory to install Java, Neo Forge, etc. + """ + @classmethod + def validate(cls, path: str): + if os.path.exists(path) and not os.path.isdir(path): + raise ValueError(f"'{path}' must be a folder") + + server_directory: ServerDirectory = ServerDirectory("Minecraft AP Server Directory") + max_heap_size: str = "2G" + min_heap_size: str = "1G" + release_channel: ReleaseChannel = ReleaseChannel("release") + java: JavaExecutable = JavaExecutable("") + + +class MinecraftWebWorld(WebWorld): + theme = "jungle" + bug_report_page = "https://github.com/qixils/NeoForgeAP/issues/new?assignees=&labels=bug&template=bug_report.yaml&title=%5BBug%5D%3A+Brief+Description+of+bug+here" + + setup = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Archipelago Minecraft software on your computer. This guide covers" + "single-player, multiworld, and related software.", + "English", + "minecraft_en.md", + "minecraft/en", + ["qixils"] + ) + + setup_es = Tutorial( + setup.tutorial_name, + setup.description, + "Español", + "minecraft_es.md", + "minecraft/es", + ["Edos"] + ) + + setup_sv = Tutorial( + setup.tutorial_name, + setup.description, + "Swedish", + "minecraft_sv.md", + "minecraft/sv", + ["Albinum"] + ) + + setup_fr = Tutorial( + setup.tutorial_name, + setup.description, + "Français", + "minecraft_fr.md", + "minecraft/fr", + ["TheLynk"] + ) + + tutorials = [setup, setup_es, setup_sv, setup_fr] + + +class MinecraftWorld(World): + """ + Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine, + craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient + structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim + victory! + """ + game = "Minecraft" + options_dataclass = MinecraftOptions + options: MinecraftOptions + settings: typing.ClassVar[MinecraftSettings] + topology_present = True + web = MinecraftWebWorld() + + item_name_to_id = Constants.item_name_to_id + location_name_to_id = Constants.location_name_to_id + + using_ut: bool + passthrough: dict[str, Any] + ut_can_gen_without_yaml = True + + def _get_mc_data(self) -> Dict[str, Any]: + exits = [connection[0] for connection in Constants.region_info["default_connections"]] + return { + # Mod data + 'world_seed': self.random.getrandbits(32), + 'seed_name': self.multiworld.seed_name, + 'player_name': self.player_name, + 'player_id': self.player, + 'client_version': client_version, + 'structures': {exit: self.multiworld.get_entrance(exit, self.player).connected_region.name for exit in exits}, + 'advancement_goal': self.options.advancement_goal.value, + 'egg_shards_required': min(self.options.egg_shards_required.value, + self.options.egg_shards_available.value), + 'egg_shards_available': self.options.egg_shards_available.value, + 'required_bosses': self.options.required_bosses.current_key, + 'MC35': bool(self.options.send_defeated_mobs.value), + 'death_link': bool(self.options.death_link.value), + 'unlockable_hearts': bool(self.options.unlockable_hearts.value), + 'starting_items': json.dumps(self.options.starting_items.value), + 'race': self.multiworld.is_race, + + # Universal Tracker data + 'bosses_to_defeat': self.options.required_bosses.value, + 'shuffle_structures': self.options.shuffle_structures.value, + 'structure_compasses': self.options.structure_compasses.value, + 'combat_difficulty': self.options.combat_difficulty.value, + 'include_hard_advancements': self.options.include_hard_advancements.value, + 'include_unreasonable_advancements': self.options.include_unreasonable_advancements.value, + 'include_postgame_advancements': self.options.include_postgame_advancements.value, + } + + def generate_early(self: "MinecraftWorld") -> None: + re_gen_passthrough = getattr(self.multiworld, "re_gen_passthrough", {}) + if re_gen_passthrough and self.game in re_gen_passthrough: + self.using_ut = True + self.passthrough = re_gen_passthrough["Minecraft"] + self.options.advancement_goal.value = self.passthrough["advancement_goal"] + self.options.egg_shards_required.value = self.passthrough["egg_shards_required"] + self.options.egg_shards_available.value = self.passthrough["egg_shards_available"] + self.options.required_bosses.value = self.passthrough["bosses_to_defeat"] + self.options.shuffle_structures.value = self.passthrough["shuffle_structures"] + self.options.structure_compasses.value = self.passthrough["structure_compasses"] + self.options.combat_difficulty.value = self.passthrough["combat_difficulty"] + self.options.include_hard_advancements.value = self.passthrough["include_hard_advancements"] + self.options.include_unreasonable_advancements.value = self.passthrough["include_unreasonable_advancements"] + self.options.include_postgame_advancements.value = self.passthrough["include_postgame_advancements"] + self.options.unlockable_hearts.value = self.passthrough.get("unlockable_hearts", 0) + self.options.death_link.value = self.passthrough["death_link"] + else: + self.using_ut = False + + def create_item(self, name: str) -> Item: + item_class = ItemClassification.filler + if name in Constants.item_info["progression_items"]: + item_class |= ItemClassification.progression + if name in Constants.item_info["useful_items"]: + item_class |= ItemClassification.useful + if name in Constants.item_info["trap_items"]: + item_class |= ItemClassification.trap + + return MinecraftItem(name, item_class, self.item_name_to_id.get(name, None), self.player) + + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = MinecraftLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) + + def create_event_item(self, name: str) -> Item: + item = self.create_item(name) + item.classification = ItemClassification.progression + return item + + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + for exit_name in exits: + r.exits.append(Entrance(self.player, exit_name, r)) + self.multiworld.regions.append(r) + + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) + + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + loc = MinecraftLocation(self.player, loc_name, + self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) + + # Add events + self.create_event("Nether Fortress", "Blaze Rods") + self.create_event("The End", "Ender Dragon") + self.create_event("Nether Fortress", "Wither") + + # Shuffle the connections + shuffle_structures(self) + + def create_items(self) -> None: + self.multiworld.itempool += build_item_pool(self) + + set_rules = set_rules + + def generate_output(self, output_directory: str) -> None: + data = self._get_mc_data() + filename = self.multiworld.get_out_file_name_base(self.player) + MinecraftContainer.patch_file_ending + + container = MinecraftContainer(data, + filename, + os.path.join(output_directory, filename), + self.player, + self.multiworld.get_file_safe_player_name(self.player), + ) + container.write() + + def fill_slot_data(self) -> dict: + return self._get_mc_data() + + def get_filler_item_name(self) -> str: + return get_junk_item_names(self.random, 1)[0] + + # For UT + @staticmethod + def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]: + return slot_data + + +class MinecraftLocation(Location): + game = "Minecraft" + +class MinecraftItem(Item): + game = "Minecraft" + + +def mc_update_output(raw_data, server, port): + data = json.loads(b64decode(raw_data)) + data['server'] = server + data['port'] = port + return b64encode(bytes(json.dumps(data), 'utf-8')) diff --git a/apworld_src/minecraft/archipelago.json b/apworld_src/minecraft/archipelago.json new file mode 100644 index 0000000..66ce26e --- /dev/null +++ b/apworld_src/minecraft/archipelago.json @@ -0,0 +1 @@ +{"game": "Minecraft", "authors": ["qixils", "Seafo", "cjmang"], "minimum_ap_version": "0.6.3", "world_version": "2.1.3", "compatible_version": 7, "version": 7} \ No newline at end of file diff --git a/apworld_src/minecraft/assets/delete.png b/apworld_src/minecraft/assets/delete.png new file mode 100644 index 0000000..83c044b Binary files /dev/null and b/apworld_src/minecraft/assets/delete.png differ diff --git a/apworld_src/minecraft/assets/edit.png b/apworld_src/minecraft/assets/edit.png new file mode 100644 index 0000000..e482752 Binary files /dev/null and b/apworld_src/minecraft/assets/edit.png differ diff --git a/apworld_src/minecraft/assets/icon.png b/apworld_src/minecraft/assets/icon.png new file mode 100644 index 0000000..6b8fc54 Binary files /dev/null and b/apworld_src/minecraft/assets/icon.png differ diff --git a/apworld_src/minecraft/assets/mcicon.png b/apworld_src/minecraft/assets/mcicon.png new file mode 100644 index 0000000..6b8fc54 Binary files /dev/null and b/apworld_src/minecraft/assets/mcicon.png differ diff --git a/apworld_src/minecraft/data/excluded_locations.json b/apworld_src/minecraft/data/excluded_locations.json new file mode 100644 index 0000000..e182379 --- /dev/null +++ b/apworld_src/minecraft/data/excluded_locations.json @@ -0,0 +1,47 @@ +{ + "hard": [ + "Very Very Frightening", + "A Furious Cocktail", + "Two by Two", + "Two Birds, One Arrow", + "Arbalistic", + "Monsters Hunted", + "Beaconator", + "A Balanced Diet", + "Uneasy Alliance", + "Cover Me in Debris", + "A Complete Catalogue", + "Surge Protector", + "Sound of Music", + "Star Trader", + "When the Squad Hops into Town", + "With Our Powers Combined!", + "Smithing with Style", + "Careful Restoration", + "The Whole Pack", + "Blowback", + "Over-Overkill", + "Stay Hydrated!", + "Heart Transplanter" + ], + "unreasonable": [ + "How Did We Get Here?", + "Adventuring Time" + ], + "ender_dragon": [ + "Free the End", + "The Next Generation", + "The End... Again...", + "You Need a Mint", + "Monsters Hunted", + "Is It a Plane?" + ], + "wither": [ + "Withering Heights", + "Bring Home the Beacon", + "Beaconator", + "A Furious Cocktail", + "How Did We Get Here?", + "Monsters Hunted" + ] +} diff --git a/apworld_src/minecraft/data/items.json b/apworld_src/minecraft/data/items.json new file mode 100644 index 0000000..5b4cdb9 --- /dev/null +++ b/apworld_src/minecraft/data/items.json @@ -0,0 +1,158 @@ +{ + "all_items": [ + "Archery", + "Progressive Resource Crafting", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "8 Emeralds", + "4 Emeralds", + "Channeling Book", + "Silk Touch Book", + "Sharpness III Book", + "Piercing IV Book", + "Looting III Book", + "Infinity Book", + "4 Diamond Ore", + "16 Iron Ore", + "500 XP", + "100 XP", + "50 XP", + "3 Ender Pearls", + "4 Lapis Lazuli", + "16 Porkchops", + "8 Gold Ore", + "Rotten Flesh", + "Single Arrow", + "32 Arrows", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Shulker Box", + "Dragon Egg Shard", + "Spyglass", + "Lead", + "Bee Trap", + "Brush", + "Structure Compass (Ocean Monument)", + "Structure Compass (Woodland Mansion)", + "Structure Compass (Ancient City)", + "Structure Compass (Trail Ruins)", + "Structure Compass (Trial Chambers)", + "Heart 1", + "Heart 2", + "Heart 3", + "Heart 4", + "Heart 5", + "Heart 6", + "Heart 7", + "Heart 8", + "Heart 9" + ], + "progression_items": [ + "Archery", + "Progressive Resource Crafting", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "Channeling Book", + "Silk Touch Book", + "Piercing IV Book", + "3 Ender Pearls", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Dragon Egg Shard", + "Spyglass", + "Lead", + "Brush", + "Structure Compass (Ocean Monument)", + "Structure Compass (Woodland Mansion)", + "Structure Compass (Ancient City)", + "Structure Compass (Trail Ruins)", + "Structure Compass (Trial Chambers)", + "Heart 1", + "Heart 2", + "Heart 3", + "Heart 4", + "Heart 5", + "Heart 6", + "Heart 7", + "Heart 8", + "Heart 9" + ], + "useful_items": [ + "Sharpness III Book", + "Looting III Book", + "Infinity Book", + "Shulker Box" + ], + "trap_items": [ + "Bee Trap" + ], + "required_pool": { + "Archery": 1, + "Progressive Resource Crafting": 2, + "Brewing": 1, + "Enchanting": 1, + "Bucket": 1, + "Flint and Steel": 1, + "Bed": 1, + "Bottles": 1, + "Shield": 1, + "Fishing Rod": 1, + "Campfire": 1, + "Progressive Weapons": 3, + "Progressive Tools": 3, + "Progressive Armor": 2, + "8 Netherite Scrap": 2, + "Channeling Book": 1, + "Silk Touch Book": 1, + "Sharpness III Book": 1, + "Piercing IV Book": 1, + "Looting III Book": 1, + "Infinity Book": 1, + "3 Ender Pearls": 4, + "Saddle": 1, + "Shulker Box": 4, + "Spyglass": 1, + "Lead": 1, + "Brush": 1 + }, + "junk_weights": { + "4 Emeralds": 2, + "4 Diamond Ore": 1, + "16 Iron Ore": 1, + "50 XP": 4, + "16 Porkchops": 2, + "8 Gold Ore": 1, + "Rotten Flesh": 1, + "32 Arrows": 1 + } +} diff --git a/apworld_src/minecraft/data/locations.json b/apworld_src/minecraft/data/locations.json new file mode 100644 index 0000000..527f09f --- /dev/null +++ b/apworld_src/minecraft/data/locations.json @@ -0,0 +1,304 @@ +{ + "all_locations": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Free the End", + "A Furious Cocktail", + "Best Friends Forever", + "Bring Home the Beacon", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Local Brewery", + "The Next Generation", + "Fishy Business", + "Hot Tourist Destinations", + "This Boat Has Legs", + "Sniper Duel", + "Nether", + "Great View From Up Here", + "How Did We Get Here?", + "Bullseye", + "Spooky Scary Skeleton", + "Two by Two", + "Stone Age", + "Two Birds, One Arrow", + "We Need to Go Deeper", + "Who's the Pillager Now?", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "The City at the End of the Game", + "Ice Bucket Challenge", + "Remote Getaway", + "Into Fire", + "War Pigs", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "The End... Again...", + "Acquire Hardware", + "Not Quite \"Nine\" Lives", + "Cover Me with Diamonds", + "Sky's the Limit", + "Hired Help", + "Return to Sender", + "Sweet Dreams", + "You Need a Mint", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Voluntary Exile", + "Eye Spy", + "The End", + "Serious Dedication", + "Postmortal", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Those Were the Days", + "Hero of the Village", + "Hidden in the Depths", + "Beaconator", + "Withering Heights", + "A Balanced Diet", + "Subspace Bubble", + "Husbandry", + "Country Lode, Take Me Home", + "Bee Our Guest", + "What a Deal!", + "Uneasy Alliance", + "Diamonds!", + "A Terrible Fortress", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "Cover Me in Debris", + "The End?", + "The Parrots and the Bats", + "A Complete Catalogue", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Overkill", + "Librarian", + "Overpowered", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Is It a Balloon?", + "Is It a Plane?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Feels Like Home", + "Sound of Music", + "Star Trader", + "Birthday Song", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town", + "With Our Powers Combined!", + "You've Got a Friend in Me", + "Smells Interesting", + "Little Sniffs", + "Planting the Past", + "Crafting a New Look", + "Smithing with Style", + "Respecting the Remnants", + "Careful Restoration", + "The Power of Books", + "Isn't It Scute?", + "Shear Brilliance", + "Good as New", + "The Whole Pack", + "Minecraft: Trial(s) Edition", + "Under Lock and Key", + "Blowback", + "Who Needs Rockets?", + "Crafters Crafting Crafters", + "Lighten Up", + "Over-Overkill", + "Revaulting", + "Stay Hydrated!", + "Heart Transplanter", + "Mob Kabob" + ], + "locations_by_region": { + "Overworld": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Best Friends Forever", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Fishy Business", + "Sniper Duel", + "Bullseye", + "Stone Age", + "Two Birds, One Arrow", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "Ice Bucket Challenge", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "Acquire Hardware", + "Cover Me with Diamonds", + "Hired Help", + "Sweet Dreams", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Eye Spy", + "Postmortal", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Husbandry", + "Country Lode, Take Me Home", + "Bee Our Guest", + "Diamonds!", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "The Parrots and the Bats", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Overkill", + "Librarian", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Sound of Music", + "Birthday Song", + "Bukkit Bukkit", + "When the Squad Hops into Town", + "You've Got a Friend in Me", + "Crafting a New Look", + "Smithing with Style", + "Respecting the Remnants", + "Careful Restoration", + "Isn't It Scute?", + "Shear Brilliance", + "Good as New", + "The Whole Pack", + "Crafters Crafting Crafters", + "Lighten Up", + "Stay Hydrated!", + "Heart Transplanter", + "Mob Kabob" + ], + "The Nether": [ + "Hot Tourist Destinations", + "This Boat Has Legs", + "Nether", + "Two by Two", + "We Need to Go Deeper", + "Not Quite \"Nine\" Lives", + "Return to Sender", + "Hidden in the Depths", + "Subspace Bubble", + "Uneasy Alliance", + "Is It a Balloon?", + "Feels Like Home", + "With Our Powers Combined!", + "The Power of Books" + ], + "The End": [ + "Free the End", + "The Next Generation", + "Remote Getaway", + "The End... Again...", + "You Need a Mint", + "The End", + "The End?", + "Is It a Plane?" + ], + "Village": [ + "Hero of the Village", + "A Balanced Diet", + "What a Deal!", + "A Complete Catalogue", + "Star Trader" + ], + "Nether Fortress": [ + "A Furious Cocktail", + "Bring Home the Beacon", + "Local Brewery", + "How Did We Get Here?", + "Spooky Scary Skeleton", + "Into Fire", + "Beaconator", + "Withering Heights", + "A Terrible Fortress" + ], + "Pillager Outpost": [ + "Who's the Pillager Now?", + "Voluntary Exile" + ], + "Bastion Remnant": [ + "War Pigs", + "Serious Dedication", + "Those Were the Days", + "Cover Me in Debris", + "Overpowered" + ], + "End City": [ + "Great View From Up Here", + "The City at the End of the Game", + "Sky's the Limit" + ], + "Ocean Monument": [ + "A Throwaway Joke", + "Smells Interesting", + "Little Sniffs", + "Planting the Past" + ], + "Ancient City": [ + "It Spreads", + "Sneak 100" + ], + "Trail Ruins": [ + "Is It a Bird?" + ], + "Trial Chambers": [ + "Minecraft: Trial(s) Edition", + "Under Lock and Key", + "Blowback", + "Who Needs Rockets?", + "Over-Overkill", + "Revaulting" + ] + } +} diff --git a/apworld_src/minecraft/data/regions.json b/apworld_src/minecraft/data/regions.json new file mode 100644 index 0000000..b809b92 --- /dev/null +++ b/apworld_src/minecraft/data/regions.json @@ -0,0 +1,38 @@ +{ + "regions": [ + ["Menu", ["New World"]], + ["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2", "Ocean", "Dark Forest", "Deep Dark", "Ruins", "Underground"]], + ["The Nether", ["Nether Structure 1", "Nether Structure 2"]], + ["The End", ["The End Structure"]], + ["Village", []], + ["Pillager Outpost", []], + ["Nether Fortress", []], + ["Bastion Remnant", []], + ["End City", []], + ["Ocean Monument", []], + ["Woodland Mansion", []], + ["Ancient City", []], + ["Trail Ruins", []], + ["Trial Chambers", []] + ], + "mandatory_connections": [ + ["New World", "Overworld"], + ["Nether Portal", "The Nether"], + ["End Portal", "The End"], + ["Ocean", "Ocean Monument"], + ["Dark Forest", "Woodland Mansion"], + ["Deep Dark", "Ancient City"], + ["Ruins", "Trail Ruins"], + ["Underground", "Trial Chambers"] + ], + "default_connections": [ + ["Overworld Structure 1", "Village"], + ["Overworld Structure 2", "Pillager Outpost"], + ["Nether Structure 1", "Nether Fortress"], + ["Nether Structure 2", "Bastion Remnant"], + ["The End Structure", "End City"] + ], + "illegal_connections": { + "Nether Fortress": ["The End Structure"] + } +} diff --git a/apworld_src/minecraft/docs/en_Minecraft.md b/apworld_src/minecraft/docs/en_Minecraft.md new file mode 100644 index 0000000..af15d68 --- /dev/null +++ b/apworld_src/minecraft/docs/en_Minecraft.md @@ -0,0 +1,132 @@ +# Minecraft + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which +structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item +checks, and occasionally when completing your own advancements. See below for which recipes are shuffled. + +## What is considered a location check in Minecraft? + +Location checks are completed when the player completes various Minecraft advancements. Opening the advancements menu +in-game by pressing "L" will display outstanding advancements. + +## When the player receives an item, what happens? + +When the player receives an item in Minecraft, it either unlocks crafting recipes or puts items into the player's +inventory directly. + +## What is the victory condition? + +Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits +sequence either by skipping it or watching it play out. + +Depending on configuration, victory may instead be achieved after defeating the Wither, +or after completing both conditions. + +## Which recipes are locked? + +* Archery + * Bow + * Arrow + * Crossbow +* Brewing + * Blaze Powder + * Brewing Stand +* Enchanting + * Enchanting Table + * Bookshelf +* Bucket +* Flint & Steel +* All Beds +* Bottles +* Shield +* Fishing Rod + * Fishing Rod + * Carrot on a Stick + * Warped Fungus on a Stick +* Campfire + * Campfire + * Soul Campfire +* Saddle +* Spyglass +* Lead +* Brush +* Progressive Weapons + * Tier I + * Stone Sword + * Stone Axe + * Stone Spear + * Copper Sword + * Copper Axe + * Copper Spear + * Tier II + * Iron Sword + * Iron Axe + * Iron Spear + * Tier III + * Diamond Sword + * Diamond Axe + * Diamond Spear +* Progessive Tools + * Tier I + * Stone Pickaxe + * Stone Shovel + * Stone Hoe + * Copper Pickaxe + * Copper Shovel + * Copper Hoe + * Tier II + * Iron Pickaxe + * Iron Shovel + * Iron Hoe + * Tier III + * Diamond Pickaxe + * Diamond Shovel + * Diamond Hoe + * Netherite Ingot +* Progressive Armor + * Tier I + * Iron Helmet + * Iron Chestplate + * Iron Leggings + * Iron Boots + * Tier II + * Diamond Helmet + * Diamond Chestplate + * Diamond Leggings + * Diamond Boots +* Progressive Resource Crafting + * Tier I + * Copper Ingot from Nuggets + * Copper Nugget + * Iron Ingot from Nuggets + * Iron Nugget + * Gold Ingot from Nuggets + * Gold Nugget + * Furnace + * Blast Furnace + * Tier II + * Redstone + * Redstone Block + * Glowstone + * Iron Ingot from Iron Block + * Iron Block + * Gold Ingot from Gold Block + * Gold Block + * Diamond + * Diamond Block + * Netherite Block + * Netherite Ingot from Netherite Block + * Anvil + * Emerald + * Emerald Block + * Copper Ingot from Copper Block + * Copper Block + * Resin Clump + * Resin Block diff --git a/apworld_src/minecraft/docs/minecraft_en.md b/apworld_src/minecraft/docs/minecraft_en.md new file mode 100644 index 0000000..fb595c0 --- /dev/null +++ b/apworld_src/minecraft/docs/minecraft_en.md @@ -0,0 +1,92 @@ +# Minecraft Randomizer Setup Guide + +## Required Software + +- Minecraft Java Edition from + the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a YAML file? + +You can customize your options by visiting the [Minecraft Player Options Page](/games/Minecraft/player-options) + +## Joining a MultiWorld Game + +### Obtain Your Minecraft Data File + +**Only one YAML file needs to be submitted per Minecraft world regardless of how many players play on it.** +All players on the world will work on progression together. + +When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your data file, or with a zip file containing everyone's data +files. Your data file should have a `.apmc` extension. + +Double-click on your `.apmc` file to have the Minecraft client auto-launch the installed NeoForge server. Make sure to +leave this window open as this is your server console. + +### Connect to the MultiServer + +Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address. + +If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect`. + +Otherwise, once you are in-game, type `/connect (Port) (Password)`, where `` is the address of the +Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. +Note that there is no colon between `` and `(Port)`. +`(Password)` is only required if the Archipelago server you are using has a password set. + +_Example usage: `/connect archipelago.gg 38281 password123`_ + +### Play the game + +When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a +multiworld game! At this point any additional minecraft players may connect to your NeoForge server. To start the game once +everyone is ready use the command `/start`. + +## Non-Windows Installation + +The Minecraft Client will install NeoForge and the Archipelago mod. +On supported operating systems, it will additionally download a local copy of the Temurin JRE. + +For other operating systems, you will need to have a Java installation available on your system path to install and launch the server. +Head to [minecraft_versions.json] +to see which Java version is required. +When in doubt, the latest release of Java should almost always work on modern (1.17+) releases of Minecraft. + +- Install the matching Java JRE + - see [Manual Installation Software Links](#manual-installation-software-links) + - or package manager provided by your OS / distribution +- For non-installed downloads, open your `host.yaml` and add the path to your Java below the `minecraft_options` key + - `java: "path/to/jdk-XX.X.X+X-jre/bin/java"` +- Run the Minecraft Client and select your .apmc file + +## Full Manual Installation + +If you're crafty, the Archipelago client is fully optional. +The following steps may help you setup an Archipelago on a remotely hosted server. +Exact version numbers are omitted; refer to [minecraft_versions.json] for specifics. + +- Download and install a compatible version of Java +- Download and install a local NeoForge server +- Download the NeoForgeAP mod and copy it to your `mods` folder +- Download your randomized .apmc file and copy it to a new `APData` folder + +### Manual Installation Software Links + +- [Minecraft NeoForge Download Page](https://neoforged.net/) +- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/qixils/NeoForgeAP/releases) + - Please note that the mod runs only on the server side and is not intended to be installed to your Minecraft client. +- [Java JRE (Temurin)](https://adoptium.net/temurin/releases?arch=x64&package=jre) + - Download the "JRE" package for your operating system + + + + +[minecraft_version.json](https://raw.githubusercontent.com/qixils/NeoForgeAP/main/versions/minecraft_versions.json) diff --git a/apworld_src/minecraft/docs/minecraft_es.md b/apworld_src/minecraft/docs/minecraft_es.md new file mode 100644 index 0000000..30ed690 --- /dev/null +++ b/apworld_src/minecraft/docs/minecraft_es.md @@ -0,0 +1,148 @@ +# Guia instalación de Minecraft Randomizer + +# Instalacion automatica para el huesped de partida + +- descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and activa el + modulo `Minecraft Client` + +## Software Requerido + +- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) + +## Configura tu fichero YAML + +### Que es un fichero YAML y potque necesito uno? + +Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu +juego. Cada jugador de un multiworld entregara u propio fichero YAML. Esto permite que cada jugador disfrute de una +experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld pueden tener diferentes opciones + +### Where do I get a YAML file? + +Un fichero basico yaml para minecraft tendra este aspecto. + +```yaml +description: Basic Minecraft Yaml +# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y +# hay un limite de 16 caracteres +name: TuNombre +game: Minecraft + +# Opciones compartidas por todos los juegos: +accessibility: full +progression_balancing: 50 +# Opciones Especficicas para Minecraft + +Minecraft: + # Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego. + advancement_goal: 50 + + # Numero de trozos de huevo de dragon a obtener (30 max) antes de que el Ender Dragon aparezca. + egg_shards_required: 10 + + # Numero de huevos disponibles en la partida (30 max). + egg_shards_available: 15 + + # Modifica el nivel de objetos logicamente requeridos para + # explorar areas peligrosas y luchar contra jefes. + combat_difficulty: + easy: 0 + normal: 1 + hard: 0 + + # Si off, los logros que dependan de suerte o sean tediosos tendran objetos de apoyo, no necesarios para completar el juego. + include_hard_advancements: + on: 0 + off: 1 + + # Si off, los logros muy dificiles tendran objetos de apoyo, no necesarios para completar el juego. + # Solo afecta a How Did We Get Here? and Adventuring Time. + include_insane_advancements: + on: 0 + off: 1 + + # Algunos logros requieren derrotar al Ender Dragon; + # Si esto se queda en off, dichos logros no tendran objetos necesarios. + include_postgame_advancements: + on: 0 + off: 1 + + # Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END. + shuffle_structures: + on: 0 + off: 1 + + # Añade brujulas de estructura al juego, + # apuntaran a la estructura correspondiente mas cercana. + structure_compasses: + on: 0 + off: 1 + + # Reemplaza un porcentaje de objetos innecesarios por trampas abeja + # las cuales crearan multiples abejas agresivas alrededor de los jugadores cuando se reciba. + bee_traps: + 0: 1 + 25: 0 + 50: 0 + 75: 0 + 100: 0 +``` + +## Unirse a un juego MultiWorld + +### Obten tu ficheros de datos Minecraft + +**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.** + +Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego +multiworld (no confundir con hospedar el mundo minecraft). Una vez la generación acabe, el anfitrión te dará un enlace a +tu fichero de datos o un zip con los ficheros de todos. Tu fichero de datos tiene una extensión `.apmc`. + +Haz doble click en tu fichero `.apmc` para que se arranque el cliente de minecraft y el servidor forge se ejecute. + +### Conectar al multiserver + +Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP +tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft. + +Una vez en juego introduce `/connect (Port) ()` donde `` es la dirección del +servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281. +`()` +solo se necesita si el servidor Archipleago tiene un password activo. + +### Jugar al juego + +Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades por unirte +exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor +forge. + +## Procedimiento de instalación manual + +Solo es requerido si quieres usar una instalacion de forge por ti mismo, recomendamos usar el instalador de Archipelago + +### Software Requerido + +- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html) +- [Minecraft Archipelago Randomizer Mod](https://github.com/qixils/NeoForgeAP/releases) + **NO INSTALES ESTO EN TU CLIENTE MINECRAFT** + +### Instalación de servidor dedicado + +Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a +él. + +1. Descarga el instalador de **Minecraft Forge** 1.16.5 desde el enlace proporcionado, siempre asegurandose de bajar la + version mas reciente. + +2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**. + - En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente + paso. + +3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar` + - La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo + en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea + a `eula=true` para aceptar el EULA y poder utilizar el software de servidor. + - Esto creara la estructura de directorios apropiada para el siguiente paso + +4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods` + - Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar diff --git a/apworld_src/minecraft/docs/minecraft_fr.md b/apworld_src/minecraft/docs/minecraft_fr.md new file mode 100644 index 0000000..15b6d5a --- /dev/null +++ b/apworld_src/minecraft/docs/minecraft_fr.md @@ -0,0 +1,74 @@ +# Guide de configuration du randomiseur Minecraft + +## Logiciel requis + +- Minecraft Java Edition à partir de + la [page de la boutique Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- Archipelago depuis la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + - (sélectionnez `Minecraft Client` lors de l'installation.) + +## Configuration de votre fichier YAML + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide sur la configuration d'un YAML de base lors de la configuration d'Archipelago +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) + +### Où puis-je obtenir un fichier YAML ? + +Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-options) + +## Rejoindre une partie MultiWorld + +### Obtenez votre fichier de données Minecraft + +**Un seul fichier yaml doit être soumis par monde minecraft, quel que soit le nombre de joueurs qui y jouent.** + +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois cela fait, +l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun +des dossiers. Votre fichier de données doit avoir une extension `.apmc`. + +Double-cliquez sur votre fichier `.apmc` pour que le client Minecraft lance automatiquement le serveur forge installé. Assurez-vous de +laissez cette fenêtre ouverte car il s'agit de votre console serveur. + +### Connectez-vous au multiserveur + +Ouvrez Minecraft, accédez à "Multijoueur> Connexion directe" et rejoignez l'adresse du serveur "localhost". + +Si vous utilisez le site Web pour héberger le jeu, il devrait se connecter automatiquement au serveur AP sans avoir besoin de `/connect` + +sinon, une fois que vous êtes dans le jeu, tapez `/connect (Port) (Password)` où `` est l'adresse du +Serveur Archipelago. `(Port)` n'est requis que si le serveur Archipelago n'utilise pas le port par défaut 38281. Notez qu'il n'y a pas de deux-points entre `` et `(Port)` mais un espace. +`(Mot de passe)` n'est requis que si le serveur Archipelago que vous utilisez a un mot de passe défini. + +### Jouer le jeu + +Lorsque la console vous indique que vous avez rejoint la salle, vous êtes prêt. Félicitations pour avoir rejoint avec succès un +jeu multimonde ! À ce stade, tous les joueurs minecraft supplémentaires peuvent se connecter à votre serveur forge. Pour commencer le jeu une fois +que tout le monde est prêt utilisez la commande `/start`. + +## Installation non Windows + +Le client Minecraft installera forge et le mod pour d'autres systèmes d'exploitation, mais Java doit être fourni par l' +utilisateur. Rendez-vous sur [minecraft_versions.json sur le MC AP GitHub](https://raw.githubusercontent.com/qixils/NeoForgeAP/master/versions/minecraft_versions.json) +pour voir quelle version de Java est requise. Les nouvelles installations utiliseront par défaut la version "release" la plus élevée. +- Installez le JDK Amazon Corretto correspondant + - voir les [Liens d'installation manuelle du logiciel](#manual-installation-software-links) + - ou gestionnaire de paquets fourni par votre OS/distribution +- Ouvrez votre `host.yaml` et ajoutez le chemin vers votre Java sous la clé `minecraft_options` + - ` java : "chemin/vers/java-xx-amazon-corretto/bin/java"` +- Exécutez le client Minecraft et sélectionnez votre fichier .apmc + +## Installation manuelle complète + +Il est fortement recommandé d'utiliser le programme d'installation d'Archipelago pour gérer l'installation du serveur forge pour vous. +Le support ne sera pas fourni pour ceux qui souhaitent installer manuellement forge. Pour ceux d'entre vous qui savent comment faire et qui souhaitent le faire, +les liens suivants sont les versions des logiciels que nous utilisons. + +### Liens d'installation manuelle du logiciel + +- [Page de téléchargement de Minecraft Forge] (https://files.minecraftforge.net/net/minecraftforge/forge/) +- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/qixils/NeoForgeAP/releases) + - **NE PAS INSTALLER CECI SUR VOTRE CLIENT** +- [Amazon Corretto](https://docs.aws.amazon.com/corretto/) + - choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche diff --git a/apworld_src/minecraft/docs/minecraft_sv.md b/apworld_src/minecraft/docs/minecraft_sv.md new file mode 100644 index 0000000..acf848e --- /dev/null +++ b/apworld_src/minecraft/docs/minecraft_sv.md @@ -0,0 +1,132 @@ +# Minecraft Randomizer Uppsättningsguide + +## Nödvändig Mjukvara + +### Server Värd + +- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html) +- [Minecraft Archipelago Randomizer Mod](https://github.com/qixils/NeoForgeAP/releases) + +### Spelare + +- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) + +## Installationsprocedurer + +### Tillägnad + +Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till. + +1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste + rekommenderade versionen. + +2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**. + - På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är + viktigt för nästa steg. + +3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar` + - Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil + kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta + till `eula=true` för att acceptera Minecrafts EULA. + - Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget. + +4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge + server. + - Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela! + +### Grundläggande Spelaruppsättning + +- Köp och installera Minecraft från länken ovanför. + + **Du är klar**. + + Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela! + +### Avancerad Spelaruppsättning + +***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.*** +Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt. + +#### Rekommenderade Moddar + +- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap) + + +1. Installera och Kör Minecraft från länken ovanför minst en gång. +2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera klient**. + - Starta Minecraft Forge minst en gång för att skapa katalogerna som behövs för de nästa stegen. +3. Navigera till din Minecraft installationskatalog och placera de önskade moddarna med `.jar` i `mods` -katalogen. + - Standardinstallationskatalogerna är som följande; + - Windows `%APPDATA%\.minecraft\mods` + - macOS `~/Library/Application Support/minecraft/mods` + - Linux `~/.minecraft/mods` + +## Konfigurera Din YAML-fil + +### Vad är en YAML-fil och varför behöver jag en? + +Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur den borde +generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter varje +spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld kan ha helt olika +alternativ. + +### Vart kan jag få tag i en YAML-fil? + +En grundläggande Minecraft YAML kommer se ut så här. + +```yaml +description: Template Name +# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns. +name: YourName +game: Minecraft +accessibility: full +progression_balancing: 0 +advancement_goal: + few: 0 + normal: 1 + many: 0 +combat_difficulty: + easy: 0 + normal: 1 + hard: 0 +include_hard_advancements: + on: 0 + off: 1 +include_insane_advancements: + on: 0 + off: 1 +include_postgame_advancements: + on: 0 + off: 1 +shuffle_structures: + on: 1 + off: 0 +``` + + +## Gå med i ett Multivärld-spel + +### Skaffa din Minecraft data-fil + +**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.** + +När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När +detta är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som +innehåller allas data-filer. Din data-fil borde ha en `.apmc` -extension. + +Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut. + +### Koppla till Multiservern + +Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status genom att +skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient. När du är inne i +spelet, skriv `/connect ()` där `` är addressen av +Archipelago-servern. `()` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt +lösenord. + +### Spela spelet + +När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis att du har lykats med +att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla in till din +forge-server. + diff --git a/apworld_src/minecraft/downloader/Java.py b/apworld_src/minecraft/downloader/Java.py new file mode 100644 index 0000000..afa30f3 --- /dev/null +++ b/apworld_src/minecraft/downloader/Java.py @@ -0,0 +1,148 @@ +import logging +import shutil +import tempfile + +from . import StepsStep, SyncStep +from .Utilities import DownloadStep, FetchStep, jre_paths +from Utils import is_windows, is_linux +import os +import zipfile +import platform +import tarfile +from typing import TypedDict, Any + + +class Download(TypedDict): + checksum: str + checksum_link: str + download_count: int + link: str + metadata_link: str + name: str + signature_link: str + size: int + +class Binary(TypedDict): + architecture: str + download_count: int + heap_size: str + image_type: str + installer: Download + jvm_impl: str + os: str + package: Download + project: str + scm_ref: str + updated_at: str + +class Version(TypedDict): + build: int + major: int + minor: int + openjdk_version: str + optional: str + security: int + semver: str + +class Asset(TypedDict): + binary: Binary + release_link: str + release_name: str + vendor: str + version: Version + +class DownloadJava(StepsStep): + def __init__(self, to: str, version: int): + self.to = to + self.version = version + self.outpath = os.path.join(to, "java", jre_paths[self.version]) + self.zip_path = os.path.join(self.outpath, "jre.zip") + self.logger = logging.getLogger("MinecraftClient") + super().__init__( + f"Downloading Java {version}...", + SyncStep(self._get_api_url), + FetchStep(), + SyncStep(self._process_assets), + DownloadStep(filepath=self.zip_path), + SyncStep(self._process_extract), + ) + + def run(self, context, *args, **kwargs): + context['java_dir'] = self.outpath + super().run(context, *args, **kwargs) + + def _get_api_url(self, context: dict[str, Any]) -> str: + self.logger.info(f"Fetching Java {self.version} versions") + + system = "windows" if is_windows else "linux" if is_linux else None + if not system: + raise Exception("Unsupported operating system for Java download") + + arch = "aarch64" if platform.machine() in ["aarch64", "arm64"] else "x64" + + return f"https://api.adoptium.net/v3/assets/latest/{self.version}/hotspot?architecture={arch}&image_type=jre&os={system}&vendor=eclipse" + + def _process_assets(self, context: dict[str, Any], assets: list[Asset]) -> tuple: + data: Asset = assets[0] + os.makedirs(self.outpath, exist_ok=True) + release_path = os.path.join(self.outpath, "release") + semver = None + + if os.path.exists(release_path): + with open(release_path, 'r') as file: + info = file.read() + semver = info.split('SEMANTIC_VERSION="')[1].split('"')[0] + self.logger.info(f"Received semver: {data['version']['semver']} local ver: {semver}") + if data["version"]["semver"] == semver: + self.logger.info("Already up-to-date, skipping download") + return False + + self.logger.info(f"Downloading Java {data['version']['semver']}") + context['java_semver'] = data['version']['semver'] + return data["binary"]["package"]["link"], None, data['version']['semver'] + + def _process_extract(self,context: dict[str, Any], res): + if not res: + # download is skipped + return + + self.logger.info(f"Extracting Java {self.version}") + # TODO: this probably also needs a mac flow, for .pkg files + if is_linux: + with tempfile.TemporaryDirectory() as temp_dir: + with tarfile.open(self.zip_path, 'r') as tarball: + tarball.extractall(temp_dir, filter='tar') + items = os.listdir(temp_dir) + if len(items) == 1 and os.path.isdir(os.path.join(temp_dir, items[0])): + original_dir = os.path.join(temp_dir, items[0]) + else: + original_dir = temp_dir + shutil.copytree(original_dir, self.outpath, dirs_exist_ok=True) + else: + with zipfile.ZipFile(self.zip_path, 'r') as zip_ref: + subfolder = zip_ref.namelist()[0] + for entry in zip_ref.infolist()[1:]: + if entry.is_dir(): + continue + relative = entry.filename[len(subfolder):] + filepath = os.path.join(self.outpath, *relative.split("/")) + dirpath = os.path.dirname(filepath) + os.makedirs(dirpath, exist_ok=True) + with open(filepath, 'wb') as file: + file.write(zip_ref.read(entry.filename)) + + os.remove(self.zip_path) + +def get_java_path(to: str, version: int) -> str: + jre = jre_paths[version] + + bin = "javaw.exe" if is_windows else "java" if is_linux else None + if not bin: + raise Exception("Unsupported operating system for Java path retrieval") + + java_path = os.path.join(to, "java", jre, "bin", bin) + + if not os.path.exists(java_path): + raise Exception(f"Java {version} not found at {java_path}") + + return java_path diff --git a/apworld_src/minecraft/downloader/Modrinth.py b/apworld_src/minecraft/downloader/Modrinth.py new file mode 100644 index 0000000..02f9ac5 --- /dev/null +++ b/apworld_src/minecraft/downloader/Modrinth.py @@ -0,0 +1,56 @@ +from .Utilities import DownloadStep, FetchStep, download_file, ua_header +from . import StepsStep, SyncStep +from typing import TypedDict +import os +import requests + +class ModrinthFile(TypedDict): + url: str + primary: bool + +class ModrinthVersion(TypedDict): + id: str + game_versions: list[str] + files: list[ModrinthFile] + +class DownloadMod(StepsStep): + def __init__(self, destination_folder: str, platform: str, game_version: str, project: str, filename: str): + self.destination_folder = destination_folder + self.platform = platform + self.game_version = game_version + self.project = project + self.filename = filename + self.jar_path = os.path.join(destination_folder, filename) + + super().__init__( + SyncStep(lambda: print(f"Downloading mod {project} for {platform} {game_version}")), + FetchStep(f"https://api.modrinth.com/v2/project/{project}/version?loaders={platform}"), + SyncStep(self._process_versions), + DownloadStep(filepath=self.jar_path), + SyncStep(lambda: self.version), + ) + + def _process_versions(self, versions: list[ModrinthVersion]) -> str: + self.version = next((ver for ver in versions if self.game_version in ver["game_versions"]), None) + if not self.version: + raise Exception(f"No version found for {self.platform} {self.game_version}") + + return self.version["files"][0]["url"] + +def download_mod(destination_folder: str, platform: str, game_version: str, project: str, filename: str) -> ModrinthVersion: + print(f"Downloading mod {project} for {platform} {game_version}") + + versions = requests.get( + f"https://api.modrinth.com/v2/project/{project}/version?loaders={platform}", + headers=ua_header, + ).json() + + version = next((ver for ver in versions if game_version in ver["game_versions"]), None) + if not version: + raise Exception(f"No version found for {platform} {game_version}") + + url = version["files"][0]["url"] + jar_path = os.path.join(destination_folder, filename) + download_file(jar_path, url, version=version["id"]) + + return version diff --git a/apworld_src/minecraft/downloader/NeoForge.py b/apworld_src/minecraft/downloader/NeoForge.py new file mode 100644 index 0000000..2754d7a --- /dev/null +++ b/apworld_src/minecraft/downloader/NeoForge.py @@ -0,0 +1,166 @@ +import logging + +from .Utilities import DownloadStep, FetchStep, SubprocessStep +from .Java import get_java_path +from . import ServerInstallData, StepsStep, SyncStep, Step +from typing import TypedDict, Any, Callable +import Utils +import os +import subprocess + +class NeoVersions(TypedDict): + isSnapshot: bool + versions: list[str] + +class DownloadNeoForge(StepsStep): + def __init__(self, to: str, + eula_popup: Callable, + force_version: str | None = None, + heap: str = "2048M"): + self.name = f"Downloading {force_version if force_version else 'latest'} NeoForge..." + + self.to = to + self.force_version = (force_version.split(".", 1)[1] + ".") if force_version else None + self.heap = heap + self.installer_jar = os.path.join(to, f"neoforge-installer.jar") + self.logger = logging.getLogger("MinecraftClient") + + super().__init__( + "Installing Neo Forge", + SyncStep(lambda *args: print(f"Downloading NeoForge index")), + FetchStep("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge"), + SyncStep(self._process_versions), + ConfirmEula(eula_popup), + DownloadStep(filepath=self.installer_jar), + SyncStep(self._process_server), + SubprocessStep("Installing NeoForge"), + SyncStep(self._process_cleanup), + ) + + def _process_versions(self,context: dict[str, Any], neo_versions: NeoVersions) -> str: + self.neo_latest = next( + (ver for ver in reversed(neo_versions["versions"]) if self.force_version is None or ver.startswith(self.force_version)), + None + ) + + if not self.neo_latest: + raise Exception("No suitable NeoForge version found") + + self.minecraft = f"1.{'.'.join(self.neo_latest.split('.', 2)[:2])}" + + self.root = os.path.join(self.to, f"NeoForge {self.minecraft}") + os.makedirs(self.root, exist_ok=True) + + self.java_path = get_java_path(self.to, 21) + self.java_path_relative = os.path.relpath(self.java_path, self.root) + self.logger.info(f"Using Java at {self.java_path}") + + self.all_run = [self.java_path, f"-Xmx{self.heap}", f"-Xms{self.heap}", "@user_jvm_args.txt"] + self.windows_run = self.all_run + [f"@libraries/net/neoforged/neoforge/{self.neo_latest}/win_args.txt"] + self.unix_run = self.all_run + [f"@libraries/net/neoforged/neoforge/{self.neo_latest}/unix_args.txt"] + self.system_run = self.windows_run if Utils.is_windows else (self.unix_run if Utils.is_linux else None) + + self.mods = os.path.join(self.root, "mods") + os.makedirs(self.mods, exist_ok=True) + + self.version_path = os.path.join(self.root, "neo_version") + context['neoforge_dir'] = self.root + context['neoforge_mod_dir'] = self.mods + context['neoforge_run_args'] = self.system_run + + if os.path.exists(self.version_path): + with open(self.version_path, 'r') as f: + if f.read().strip() == self.neo_latest: + self.logger.info(f"NeoForge {self.neo_latest} is already installed, skipping download") + return False + + self.logger.info(f"Downloading NeoForge {self.neo_latest} installer") + return f"https://maven.neoforged.net/releases/net/neoforged/neoforge/{self.neo_latest}/neoforge-{self.neo_latest}-installer.jar", None, self.neo_latest + + def _process_server(self, context: dict[str, Any], req): + if req is False: + # install is skipped + return + + self.logger.info(f"Running NeoForge {self.neo_latest} installer") + java_path = get_java_path(self.to, 21) + return (java_path, "-jar", self.installer_jar, "--installServer"), {"cwd": self.root, "stderr": subprocess.DEVNULL, "stdout": subprocess.DEVNULL} + + def _process_cleanup(self, context: dict[str, Any], req): + bat_file = os.path.join(self.root, "run.bat") + sh_file = os.path.join(self.root, "run.sh") + context['neoforge_run'] = bat_file if Utils.is_windows else (sh_file if Utils.is_linux else None) + + if req is False: + # install is skipped + return ServerInstallData(root_dir=self.root, mods_dir=self.mods, run_args=self.system_run) + + os.unlink(self.installer_jar) + os.unlink(self.installer_jar + '.version') + + # we don't actually use these ourselves but they're nice to have + # (although they are not very well tested) + with open(bat_file, 'w') as f: + windows_run = list(self.windows_run) + windows_run[0] = f'&"{windows_run[0]}"' + f.write(f"@echo off\nstart {' '.join(windows_run)} %* > log.txt 2> errorlog.txt") + + with open(sh_file, 'w') as f: + unix_run = list(self.unix_run) + unix_run[0] = unix_run[0].replace(' ', '\\ ') + f.write(f"#!/bin/bash\nexec {' '.join(self.unix_run)} \"$@\" > log.txt 2> errorlog.txt") + + with open(self.version_path, 'w') as f: + f.write(self.neo_latest) + + return ServerInstallData(root_dir=self.root, mods_dir=self.mods, run_args=self.system_run) + +class ConfirmEula(Step): + + def __init__(self, confirm_prompt: Callable): + super().__init__() + self.logger = logging.Logger("MinecraftClient") + self.confirm_prompt = confirm_prompt + self.outdir: str | None = None + + def run(self, + context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable | None = None, + error_ok: bool = False): + + if not previous or not previous[0]: + on_success(False) + return + + if 'neoforge_dir' not in context: + on_success(False) + return + + self.outdir = context['neoforge_dir'] + file = os.path.join(self.outdir, "eula.txt") + if os.path.exists(file): + with open(file, 'r') as f: + if 'eula=true' in f.read(): + on_success(*previous) + return + + success_fn = lambda *args: self.confirmed(previous, on_success) + self.confirm_prompt( + confirm=success_fn, + title="Minecraft Eula", + content=""" + Please note that by running a Minecraft server, + you are indicating your agreement to Minecraft's + EULA (https://aka.ms/MinecraftEULA).""", + cancel=on_failure, + ) + + def confirmed(self, previous, on_success: Callable | None = None): + self.logger.info(f"confirmed {previous}") + contents = "eula=true" + with open(os.path.join(self.outdir, 'eula.txt'), 'w') as f: + f.write(contents) + on_success(*previous) diff --git a/apworld_src/minecraft/downloader/Utilities.py b/apworld_src/minecraft/downloader/Utilities.py new file mode 100644 index 0000000..a106a59 --- /dev/null +++ b/apworld_src/minecraft/downloader/Utilities.py @@ -0,0 +1,193 @@ +import logging +import os +import subprocess +import threading + +import certifi +import requests +from typing import Any, Callable, Optional +from . import Step +from kivy.network.urlrequest import UrlRequest, UrlRequestRequests + +ua = "qixils/minecraft-crowdcontrol/1.0.0" +ua_header = {"User-Agent": ua} + +jre_paths: dict[int, str] = { + 8: "jdk-8", + 21: "jdk-21", +} + +# TODO: headers +# TODO: redirects + +class DownloadStep(Step): + def __init__(self, url: str | None = None, filepath: str | None = None): + super().__init__() + self.filepath = filepath + self.url = url + self.logger = logging.getLogger("MinecraftClient") + + def run(self, + context: dict[str, Any], + *args, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + url = self.url + filepath = self.filepath + version = None + self.logger.info(f"Got arguments {args}") + if len(args) > 0: + url = args[0] or url + if len(args) > 1: + filepath = args[1] or filepath + if len(args) > 2: + version = args[2] or version + + if callable(url): + url = url() + if callable(filepath): + filepath = filepath() + if callable(version): + version = version() + + self.filepath = filepath + # If we were passed a blank URL then we assume this skip should be skipped for already being downloaded + if not url or not filepath: + if on_success is not None: + on_success(False) + return + + # Optionally check if a file with a matching version is already downloaded + version_path = filepath + ".version" + if version is not None: + if os.path.exists(version_path): + with open(version_path, 'r') as f: + if f.read().strip() == version: + on_success(False) + return + self.logger.info(f"Sending request to {url}; downloading to {filepath}") + # Using requests over urllib, cause redirects + UrlRequestRequests(url, + file_path=filepath, + on_progress=lambda req, current_size, total_size: on_progress is not None and on_progress(current_size / total_size if total_size > 0 else 0, f"Downloading {os.path.basename(filepath)}..."), + on_success=lambda req, res: self._on_success(res, version=version, on_success=on_success), + on_error=on_failure, + on_redirect= lambda req, res: self.logger.info(f"{req}, {res}, {req.resp_status} {req.resp_headers}"), + chunk_size=1024000, + # ca_file=certifi.where() + ) + + def _on_success(self, res, version: str | None = None, on_success: Callable | None = None): + with open(self.filepath + ".version", 'w') as f: + f.write(version) + if on_success is not None: + on_success(res or True) + +class FetchStep(Step): + def __init__(self, url: str | None = None): + super().__init__() + self.url = url + self.logger = logging.getLogger("MinecraftClient") + + def run(self, + context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + if previous: + url = previous[0] if previous[0] is not None and type(previous[0]) is str else self.url + self.logger.info(f"Requesting to url: {url}") + payload_lambda = lambda req, resp: on_success(resp) + UrlRequest(url, + on_progress=lambda req, current_size, total_size: on_progress is not None and on_progress(current_size / total_size, "Loading data..."), + on_success=payload_lambda, + on_error=on_failure, + ca_file=certifi.where()) + +class SubprocessStep(Step): + def __init__(self, name: str, *args): + super().__init__() + self.args = args + self.logger = logging.getLogger("MinecraftClient") + self.name = name + + def run(self, + context: dict[str, Any], + *previous, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable | None = None, + error_ok: bool = False): + args = previous if len(previous) > 0 else self.args + + if args is None or len(args) == 0 or not args[0]: + on_success(False) + return + self.logger.info(f"Arguments: {args}") + thread = threading.Thread(target=self._run_in_thread, args=(on_success, args)) + thread.start() + + @staticmethod + def _run_in_thread(on_exit: Callable, popen_args: tuple): + logger = logging.getLogger("MinecraftClient") + kwargs = dict() + logger.info(f"Arguments: {popen_args}") + if type(popen_args[-1]) is dict: + kwargs = popen_args[-1] + popen_args = popen_args[:-1] + logger.info(f"Arguments: {popen_args} {kwargs}") + proc = subprocess.Popen(*popen_args, **kwargs) + proc.wait() + on_exit(True) + + +def is_semver_ge(new_semver: str, old_semver: str) -> bool: + new_parts = new_semver.split(".") + old_parts = old_semver.split(".") + parts = max(len(new_parts), len(old_parts)) + + for i in range(parts): + new_part = int(new_parts[i]) if i < len(new_parts) else 0 + old_part = int(old_parts[i]) if i < len(old_parts) else 0 + + if new_part > old_part: + return True + elif new_part < old_part: + return False + + return True + +def semver_sort(a: str, b: str) -> int: + if a == b: + return 0 + if is_semver_ge(a, b): + return -1 + if is_semver_ge(b, a): + return 1 + return 0 # fallback + +def download_file(path: str, url: str, version: Optional[str] = None) -> None: + # Optionally check if a file with a matching version is already downloaded + version_path = path + ".version" + if version is not None: + if os.path.exists(version_path): + with open(version_path, 'r') as f: + if f.read().strip() == version: + return + + response = requests.get(url, stream=True) + if response.status_code != 200: + raise Exception(f"Failure while retrieving remote data (source: {url})") + + with open(path, 'wb') as file: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + + if version is not None: + with open(version_path, 'w') as f: + f.write(version) + diff --git a/apworld_src/minecraft/downloader/__init__.py b/apworld_src/minecraft/downloader/__init__.py new file mode 100644 index 0000000..5a9db79 --- /dev/null +++ b/apworld_src/minecraft/downloader/__init__.py @@ -0,0 +1,127 @@ +from typing import NamedTuple, Any, Callable +from abc import ABC, abstractmethod + +import logging + +class ServerInstallData(NamedTuple): + root_dir: str + mods_dir: str + run_args: list[str] + + +class Step(ABC): + @abstractmethod + def run(self, + context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + pass + + +class StepsStep(Step): + def __init__(self, name: str, *steps: Step): + super().__init__() + self.steps = steps + self.logger = logging.Logger("MinecraftClient") + self.name = name + + def run(self, context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + self._run_index(0, context, *previous, on_success=on_success, on_failure=on_failure, on_progress=on_progress, error_ok=error_ok) + + def _run_index(self, index: int, + context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + + self.logger.debug(f"running step {index}; {type(self)}") + + if len(self.steps) <= index: + if on_progress is not None: + on_progress(1.0, self.name) + if on_success is not None: + on_success(*previous) + return + if on_progress is not None: + on_progress(index / len(self.steps), self.name) + success_lambda = lambda *result: self._run_index(index + 1, + context, + *result if type(result) is tuple else result, + on_success=on_success, + on_failure=on_failure, + on_progress=on_progress, + error_ok=error_ok) + try: + self.steps[index].run( + context, + *previous, + on_success=success_lambda, + on_failure=on_failure if not error_ok else (lambda err: success_lambda(*previous)), + on_progress=lambda value, name: self._emit_on_progress(index=index, extra=value, name=name, on_progress=on_progress), + error_ok=error_ok, + ) + except Exception as e: + self.logger.error("Exception while performing step", exc_info=True) + if error_ok: + success_lambda(*previous) + elif on_failure is not None: + on_failure(e) + + def _emit_on_progress(self, index: int, name: str, extra: float = 0, on_progress: Callable[[float, str], None] | None = None): + if on_progress is None: + return + + if extra is None: + extra = 0 + else: + extra = max(0.0, min(1.0, extra)) + + on_progress((index / len(self.steps)) + (extra / len(self.steps)), name or self.steps[index].name or self.name) + + + + +class SyncStep(Step): + def __init__(self, fn: Callable): + super().__init__() + self.fn = fn + self.logger = logging.Logger("MinecraftClient") + + def run(self, + context: dict[str, Any], + *previous: Any, + on_success: Callable | None = None, + on_failure: Callable | None = None, + on_progress: Callable[[float, str], None] | None = None, + error_ok: bool = False): + try: + res = self.fn(context, *previous if type(previous) is tuple else previous) + self.logger.info(f"Got result {res}") + if on_success is not None: + if res is None or type(res) is not tuple: + on_success(res) + else: + on_success(*res) + except Exception as e: + self.logger.error("Step failed", exc_info=True) + if on_failure is not None: + on_failure(e) + +class BytesToStringStep(SyncStep): + def __init__(self): + super().__init__(BytesToStringStep.bytes_to_string) + + + @staticmethod + def bytes_to_string(context: dict[str, Any], data: bytes): + return data.decode('utf-8') \ No newline at end of file diff --git a/apworld_src/minecraft/layouts/minecraft.kv b/apworld_src/minecraft/layouts/minecraft.kv new file mode 100644 index 0000000..93d39d0 --- /dev/null +++ b/apworld_src/minecraft/layouts/minecraft.kv @@ -0,0 +1,312 @@ +: + size_hint: 1, None + height: 50 + spacing: 10 + + Button: + text: '[b]' +root.name + '[/b]\n[color=bbb]' + root.path + '[/color]' + halign: 'left' + padding: 10, 0 + markup: True + size: self.texture_size + id: load + text_size: self.width - 10, None + on_press: root.load() + + Button: + id: rename + size_hint: None, 1 + width: 50 + padding: 20, 0 + on_press: root.rename() + Image: + id: rename_icon + center_x: self.parent.center_x + center_y: self.parent.center_y + size: self.width, self.height + color: 1, 1, 1, 1 + fit_mode: "scale-down" + + Button: + id: delete + size_hint: None, 1 + width: 50 + padding: 20, 0 + on_press: root.delete() + Image: + id: delete_icon + center_x: self.parent.center_x + center_y: self.parent.center_y + size: self.width, self.height + color: 1, .2, .2, 1 + fit_mode: "scale-down" + + + rows: 1 + size_hint: 1, None + height: 30 + spacing: 10 + Label: + size_hint: None, 1 + width: dp(150) + text: root.label + ":" + text_size: self.size + valign: "middle" + halign: "right" + TextInput: + text: root.value + multiline: False + on_text_validate: root.button_press() + + + rows: 1 + size_hint: 1, None + height: 30 + spacing: 10 + Label: + size_hint: None, 1 + width: dp(150) + text: root.label + ":" + text_size: self.size + valign: "middle" + halign: "right" + Spinner: + text: root.value + values: root.options + size_hint: None, None + height: 30 + width: dp(200) + on_text: + root.value = self.text + + + size_hint_y: None + thickness: 2 + margin: 2 + height: self.thickness + 2 * self.margin + color: 1, 1, 1 + canvas: + Color: + rgb: self.color + Rectangle: + pos: self.x + self.margin, self.y + self.margin + 1 + size: self.width - 2 * self.margin , self.thickness + +: + auto_dismiss: False + size_hint: .5, .5 + BoxLayout: + orientation: 'vertical' + padding: 10 + Label: + text: root.text + ProgressBar: + max: root.max + value: root.progress + Label: + text: root.progress_text + +: + auto_dismiss: True + confirm_text: "Confirm" + cancel_text: "Cancel" + size_hint: .5, .5 + GridLayout: + cols: 1 + padding: 10 + BoxLayout: + padding: 10 + orientation: 'vertical' + id: content + Label: + text: root.text + + GridLayout: + rows: 1 + spacing: 10 + padding: 10, 0 + size_hint: 1, None + height: 40 + Button: + id: confirm + text: root.confirm_text + background_color: 0, 1, 0, 1 + on_press: root.dismiss() + Button + id: cancel + text: root.cancel_text + background_color: 1, 0, 0, 1 + on_press: root.dismiss() + +: + auto_dismiss: True + button_text: "OK" + size_hint: .5, .5 + GridLayout: + cols: 1 + padding: 10 + BoxLayout: + padding: 10 + orientation: 'vertical' + id: content + Label: + text: root.text + text_size: self.width, None + size_hint: 1, None + + GridLayout: + rows: 1 + spacing: 10 + padding: 10, 0 + size_hint: 1, None + height: 40 + Button: + text: root.button_text + background_color: 0, 1, 0, 1 + on_press: root.dismiss() + +: + markup: True + size_hint: 1, None + text_size: self.width, None + size: self.width, self.texture_size[1] + +: + markup: True + padding: 5,0 + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + canvas.before: + Color: + rgba: (0.2, 0.2, 0.2, 1) + Rectangle: + size: self.size + pos: self.pos + +: + messages: 1000 # amount of messages stored in client logs. + cols: 1 + viewclass: 'LogLabel' + scroll_y: 0 + scroll_type: ["content", "bars"] + bar_width: dp(12) + effect_cls: "ScrollEffect" + RecycleBoxLayout: + default_size: None, dp(20) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: dp(3) + +: + name: "Server" + background_color: .5, .1, .1 , 1 + GridLayout: + cols: 1 + padding: dp(2) + spacing: dp(2) + Label: + id: status + size_hint: 1, None + height: 50 + text: "...Server Starting..." + font_size: dp(25) + + canvas.before: + Color: + rgba: root.background_color + Rectangle: + size: self.size + pos: self.pos + ServerLog: + id: log + TextInput: + id: cmd + multiline: False + size_hint: 1, None + height: self.minimum_height + on_text_validate: root.send_command(self.text) + +: + id: 'welcome' + name: "Welcome" + padding: 10, 5, 10, 10 + TabbedPanel: + do_default_tab: False + TabbedPanelItem: + text: 'Games' + padding: 10 + GridLayout: + cols: 1 + padding: 10 + GridLayout: + rows: 1 + orientation: 'rl-tb' + size_hint: 1, None + height: 30 + Button: + text: 'Open APMC File' + padding: 10, 5 + size_hint: None, 1 + width: self.texture_size[0] + on_press: root.client.open_apmc() + + Separator: + color: .5, .5, 1 + thickness: 3 + margin: 5 + + ScrollView: + do_scroll_x: False + effect_cls: 'ScrollEffect' + GridLayout: + cols: 1 + spacing: 10 + padding: 0, 5 + size_hint_y: None + height: self.minimum_height + id: saves + TabbedPanelItem: + text: "Options" + GridLayout: + cols: 1 + id: option_layout + size_hint: 1, None + height: self.minimum_height + padding: 10 + spacing: 10 + + GridLayout: + rows: 1 + orientation: "rl-tb" + size_hint: 1, None + height: 30 + Button: + size_hint: None, 1 + width: self.texture_size[0] + 20 + text: "Save" + on_press: root.save_options() + + FolderOption: + id: path + label: "Server Path" + button_label: "Open Folder" + Button: + size_hint: None, 1 + width: self.texture_size[0] + dp(20) + text: self.parent.button_label + on_press: self.parent.button_press() + + TextOption: + id: max_memory + label: "Max Memory" + + TextOption: + id: min_memory + label: "Min Memory" + + DropdownOption: + id: release_option + label: "Release Channel" diff --git a/apworld_src/minecraft/test/__init__.py b/apworld_src/minecraft/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apworld_src/minecraft/test/bases.py b/apworld_src/minecraft/test/bases.py new file mode 100644 index 0000000..932ee83 --- /dev/null +++ b/apworld_src/minecraft/test/bases.py @@ -0,0 +1,99 @@ +from typing import ClassVar + +from BaseClasses import CollectionState, ItemClassification +from test.bases import WorldTestBase + +from .. import MinecraftOptions, MinecraftWorld + + +class MCTestBase(WorldTestBase): + game = "Minecraft" + player: ClassVar[int] = 1 + + def _create_items(self, items, player): + singleton = False + if isinstance(items, str): + items = [items] + singleton = True + ret = [self.multiworld.worlds[player].create_item(item) for item in items] + if singleton: + return ret[0] + return ret + + def _get_items(self, item_pool, all_except): + if all_except and len(all_except) > 0: + items = self.multiworld.itempool[:] + items = [item for item in items if item.name not in all_except] + items.extend(self._create_items(item_pool[0], 1)) + else: + items = self._create_items(item_pool[0], 1) + return self.get_state(items) + + def _get_items_partial(self, item_pool, missing_item): + new_items = item_pool[0].copy() + new_items.remove(missing_item) + items = self._create_items(new_items, 1) + return self.get_state(items) +# LTTP Code Minecraft was using for Unit Tests. Find a way to re implement properly (Code Author: berserker55) + _state_cache = {} + def get_state(self, items): + if (self.multiworld, tuple(items)) in self._state_cache: + return self._state_cache[self.multiworld, tuple(items)] + state = CollectionState(self.multiworld) + for item in items: + item.classification = ItemClassification.progression + state.collect(item, prevent_sweep=True) + state.sweep_for_advancements() + state.update_reachable_regions(1) + self._state_cache[self.multiworld, tuple(items)] = state + return state + + def get_path(self, state, region): + def flist_to_iter(node): + while node: + value, node = node + yield value + + def run_location_tests(self, access_pool): + for i, (location, access, *item_pool) in enumerate(access_pool): + items = item_pool[0] + all_except = item_pool[1] if len(item_pool) > 1 else None + state = self._get_items(item_pool, all_except) + path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region) + with self.subTest(msg="Reach Location", location=location, access=access, items=items, + all_except=all_except, path=path, entry=i): + + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, + f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") + + # check for partial solution + if not all_except and access: # we are not supposed to be able to reach location with partial inventory + for missing_item in item_pool[0]: + with self.subTest(msg="Location reachable without required item", location=location, + items=item_pool[0], missing_item=missing_item, entry=i): + state = self._get_items_partial(item_pool, missing_item) + + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False, + f"failed {self.multiworld.get_location(location, 1)}: succeeded with " + f"{missing_item} removed from: {item_pool}") + + def run_entrance_tests(self, access_pool): + for i, (entrance, access, *item_pool) in enumerate(access_pool): + items = item_pool[0] + all_except = item_pool[1] if len(item_pool) > 1 else None + state = self._get_items(item_pool, all_except) + path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region) + with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items, + all_except=all_except, path=path, entry=i): + + self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access) + + # check for partial solution + if not all_except and access: # we are not supposed to be able to reach location with partial inventory + for missing_item in item_pool[0]: + with self.subTest(msg="Entrance reachable without required item", entrance=entrance, + items=item_pool[0], missing_item=missing_item, entry=i): + state = self._get_items_partial(item_pool, missing_item) + self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False, + f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}") + diff --git a/apworld_src/minecraft/test/test_advancements.py b/apworld_src/minecraft/test/test_advancements.py new file mode 100644 index 0000000..4337f9e --- /dev/null +++ b/apworld_src/minecraft/test/test_advancements.py @@ -0,0 +1,1722 @@ +from ..test.bases import MCTestBase + + +# Format: +# [location, expected_result, given_items, [excluded_items]] +# Every advancement has its own test, named by its internal ID number. +class TestAdvancements(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False, + "include_hard_advancements": False, + } + + def test_42000(self): + self.run_location_tests([ + ["Who is Cutting Onions?", False, []], + ["Who is Cutting Onions?", False, [], ['Progressive Resource Crafting']], + ["Who is Cutting Onions?", False, [], ['Flint and Steel']], + ["Who is Cutting Onions?", False, [], ['Progressive Tools']], + ["Who is Cutting Onions?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Who is Cutting Onions?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Who is Cutting Onions?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42001(self): + self.run_location_tests([ + ["Oh Shiny", False, []], + ["Oh Shiny", False, [], ['Progressive Resource Crafting']], + ["Oh Shiny", False, [], ['Flint and Steel']], + ["Oh Shiny", False, [], ['Progressive Tools']], + ["Oh Shiny", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Oh Shiny", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Oh Shiny", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42002(self): + self.run_location_tests([ + ["Suit Up", False, []], + ["Suit Up", False, [], ["Progressive Armor"]], + ["Suit Up", False, [], ["Progressive Resource Crafting"]], + ["Suit Up", False, [], ["Progressive Tools"]], + ["Suit Up", True, ["Progressive Armor", "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + def test_42003(self): + self.run_location_tests([ + ["Very Very Frightening", False, []], + ["Very Very Frightening", False, [], ['Channeling Book']], + ["Very Very Frightening", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Very Very Frightening", False, [], ['Enchanting']], + ["Very Very Frightening", False, [], ['Progressive Tools']], + ["Very Very Frightening", False, [], ['Progressive Weapons']], + ["Very Very Frightening", True, ['Progressive Weapons', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Enchanting', 'Progressive Resource Crafting', 'Progressive Resource Crafting', 'Channeling Book']], + ]) + + def test_42004(self): + self.run_location_tests([ + ["Hot Stuff", False, []], + ["Hot Stuff", False, [], ["Bucket"]], + ["Hot Stuff", False, [], ["Progressive Resource Crafting"]], + ["Hot Stuff", False, [], ["Progressive Tools"]], + ["Hot Stuff", True, ["Bucket", "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + def test_42005(self): + self.run_location_tests([ + ["Free the End", False, []], + ["Free the End", False, [], ['Progressive Resource Crafting']], + ["Free the End", False, [], ['Flint and Steel']], + ["Free the End", False, [], ['Progressive Tools']], + ["Free the End", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Free the End", False, [], ['Progressive Armor']], + ["Free the End", False, [], ['Brewing']], + ["Free the End", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Free the End", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Free the End", False, [], ['Archery']], + ["Free the End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Free the End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42006(self): + self.run_location_tests([ + ["A Furious Cocktail", False, []], + ["A Furious Cocktail", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["A Furious Cocktail", False, [], ['Flint and Steel']], + ["A Furious Cocktail", False, [], ['Progressive Tools']], + ["A Furious Cocktail", False, [], ['Progressive Weapons']], + ["A Furious Cocktail", False, [], ['Progressive Armor', 'Shield']], + ["A Furious Cocktail", False, [], ['Brewing']], + ["A Furious Cocktail", False, [], ['Bottles']], + ["A Furious Cocktail", False, [], ['Fishing Rod']], + ["A Furious Cocktail", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["A Furious Cocktail", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', + 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', + 'Progressive Armor', 'Progressive Armor', + 'Enchanting', 'Brewing', 'Bottles', 'Fishing Rod']], + ]) + + def test_42007(self): + self.run_location_tests([ + ["Best Friends Forever", True, []], + ]) + + def test_42008(self): + self.run_location_tests([ + ["Bring Home the Beacon", False, []], + ["Bring Home the Beacon", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Bring Home the Beacon", False, [], ['Flint and Steel']], + ["Bring Home the Beacon", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Bring Home the Beacon", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Bring Home the Beacon", False, ['Progressive Armor'], ['Progressive Armor']], + ["Bring Home the Beacon", False, [], ['Enchanting']], + ["Bring Home the Beacon", False, [], ['Brewing']], + ["Bring Home the Beacon", False, [], ['Bottles']], + ["Bring Home the Beacon", True, [], ['Bucket']], + ["Bring Home the Beacon", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', + 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', + 'Progressive Armor', 'Progressive Armor', + 'Enchanting', 'Brewing', 'Bottles']], + ]) + + def test_42009(self): + self.run_location_tests([ + ["Not Today, Thank You", False, []], + ["Not Today, Thank You", False, [], ["Shield"]], + ["Not Today, Thank You", False, [], ["Progressive Resource Crafting"]], + ["Not Today, Thank You", False, [], ["Progressive Tools"]], + ["Not Today, Thank You", True, ["Shield", "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + def test_42010(self): + self.run_location_tests([ + ["Isn't It Iron Pick", False, []], + ["Isn't It Iron Pick", True, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools"]], + ["Isn't It Iron Pick", False, [], ["Progressive Tools", "Progressive Tools"]], + ["Isn't It Iron Pick", False, [], ["Progressive Resource Crafting"]], + ["Isn't It Iron Pick", False, ["Progressive Tools", "Progressive Resource Crafting"]], + ["Isn't It Iron Pick", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting"]], + ]) + + def test_42011(self): + self.run_location_tests([ + ["Local Brewery", False, []], + ["Local Brewery", False, [], ['Progressive Resource Crafting']], + ["Local Brewery", False, [], ['Flint and Steel']], + ["Local Brewery", False, [], ['Progressive Tools']], + ["Local Brewery", False, [], ['Progressive Weapons']], + ["Local Brewery", False, [], ['Progressive Armor', 'Shield']], + ["Local Brewery", False, [], ['Brewing']], + ["Local Brewery", False, [], ['Bottles']], + ["Local Brewery", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Local Brewery", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Local Brewery", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Local Brewery", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ["Local Brewery", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ]) + + def test_42012(self): + self.run_location_tests([ + ["The Next Generation", False, []], + ["The Next Generation", False, [], ['Progressive Resource Crafting']], + ["The Next Generation", False, [], ['Flint and Steel']], + ["The Next Generation", False, [], ['Progressive Tools']], + ["The Next Generation", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["The Next Generation", False, [], ['Progressive Armor']], + ["The Next Generation", False, [], ['Brewing']], + ["The Next Generation", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["The Next Generation", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["The Next Generation", False, [], ['Archery']], + ["The Next Generation", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The Next Generation", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42013(self): + self.run_location_tests([ + ["Fishy Business", False, []], + ["Fishy Business", False, [], ['Fishing Rod']], + ["Fishy Business", True, ['Fishing Rod']], + ]) + + def test_42014(self): + self.run_location_tests([ + ["Hot Tourist Destinations", False, []], + ["Hot Tourist Destinations", False, [], ['Progressive Resource Crafting']], + ["Hot Tourist Destinations", False, [], ['Flint and Steel']], + ["Hot Tourist Destinations", False, [], ['Progressive Tools']], + ["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Hot Tourist Destinations", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Hot Tourist Destinations", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42015(self): + self.run_location_tests([ + ["This Boat Has Legs", False, []], + ["This Boat Has Legs", False, [], ['Progressive Resource Crafting']], + ["This Boat Has Legs", False, [], ['Flint and Steel']], + ["This Boat Has Legs", False, [], ['Progressive Tools']], + ["This Boat Has Legs", False, [], ['Fishing Rod']], + ["This Boat Has Legs", False, [], ['Saddle']], + ["This Boat Has Legs", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["This Boat Has Legs", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Fishing Rod']], + ["This Boat Has Legs", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']], + ]) + + def test_42016(self): + self.run_location_tests([ + ["Sniper Duel", False, []], + ["Sniper Duel", False, [], ['Archery']], + ["Sniper Duel", True, ['Archery']], + ]) + + def test_42017(self): + self.run_location_tests([ + ["Nether", False, []], + ["Nether", False, [], ['Progressive Resource Crafting']], + ["Nether", False, [], ['Flint and Steel']], + ["Nether", False, [], ['Progressive Tools']], + ["Nether", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Nether", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Nether", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42018(self): + self.run_location_tests([ + ["Great View From Up Here", False, []], + ["Great View From Up Here", False, [], ['Progressive Resource Crafting']], + ["Great View From Up Here", False, [], ['Flint and Steel']], + ["Great View From Up Here", False, [], ['Progressive Tools']], + ["Great View From Up Here", False, [], ['Progressive Weapons']], + ["Great View From Up Here", False, [], ['Progressive Armor', 'Shield']], + ["Great View From Up Here", False, [], ['Brewing']], + ["Great View From Up Here", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Great View From Up Here", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Great View From Up Here", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Great View From Up Here", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Great View From Up Here", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Great View From Up Here", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42019(self): + self.run_location_tests([ + ["How Did We Get Here?", False, []], + ["How Did We Get Here?", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["How Did We Get Here?", False, [], ['Flint and Steel']], + ["How Did We Get Here?", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["How Did We Get Here?", False, ['Progressive Weapons', 'Progressive Weapons'], ['Progressive Weapons']], + ["How Did We Get Here?", False, ['Progressive Armor'], ['Progressive Armor']], + ["How Did We Get Here?", False, [], ['Shield']], + ["How Did We Get Here?", False, [], ['Enchanting']], + ["How Did We Get Here?", False, [], ['Brewing']], + ["How Did We Get Here?", False, [], ['Bottles']], + ["How Did We Get Here?", False, [], ['Archery']], + ["How Did We Get Here?", False, [], ['Fishing Rod']], + ["How Did We Get Here?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["How Did We Get Here?", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', 'Flint and Steel', + 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', + 'Progressive Armor', 'Progressive Armor', 'Shield', + 'Enchanting', 'Brewing', 'Archery', 'Bottles', 'Fishing Rod', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42020(self): + self.run_location_tests([ + ["Bullseye", False, []], + ["Bullseye", False, [], ['Archery']], + ["Bullseye", False, [], ['Progressive Resource Crafting']], + ["Bullseye", False, [], ['Progressive Tools']], + ["Bullseye", True, ['Progressive Tools', 'Progressive Tools', 'Progressive Resource Crafting', 'Archery']], + ]) + + def test_42021(self): + self.run_location_tests([ + ["Spooky Scary Skeleton", False, []], + ["Spooky Scary Skeleton", False, [], ['Progressive Resource Crafting']], + ["Spooky Scary Skeleton", False, [], ['Flint and Steel']], + ["Spooky Scary Skeleton", False, [], ['Progressive Tools']], + ["Spooky Scary Skeleton", False, [], ['Progressive Weapons']], + ["Spooky Scary Skeleton", False, [], ['Progressive Armor', 'Shield']], + ["Spooky Scary Skeleton", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Spooky Scary Skeleton", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor']], + ["Spooky Scary Skeleton", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor']], + ["Spooky Scary Skeleton", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']], + ["Spooky Scary Skeleton", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield']], + ]) + + def test_42022(self): + self.run_location_tests([ + ["Two by Two", False, []], + ["Two by Two", False, [], ['Progressive Resource Crafting']], + ["Two by Two", False, [], ['Flint and Steel']], + ["Two by Two", False, [], ['Progressive Tools']], + ["Two by Two", False, [], ['Progressive Weapons']], + ["Two by Two", False, [], ['Bucket']], + ["Two by Two", False, [], ['Brush']], + ["Two by Two", False, [], ['Fishing Rod']], + ["Two by Two", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Two by Two", False, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], + ["Two by Two", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Brush', 'Fishing Rod']], + ]) + + def test_42023(self): + self.run_location_tests([ + ["Stone Age", True, []], + ]) + + def test_42024(self): + self.run_location_tests([ + ["Two Birds, One Arrow", False, []], + ["Two Birds, One Arrow", False, [], ['Archery']], + ["Two Birds, One Arrow", False, [], ['Progressive Resource Crafting']], + ["Two Birds, One Arrow", False, ['Progressive Tools'], ['Progressive Tools', 'Progressive Tools']], + ["Two Birds, One Arrow", False, [], ['Enchanting']], + ["Two Birds, One Arrow", True, ['Archery', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Enchanting']], + ]) + + def test_42025(self): + self.run_location_tests([ + ["We Need to Go Deeper", False, []], + ["We Need to Go Deeper", False, [], ['Progressive Resource Crafting']], + ["We Need to Go Deeper", False, [], ['Flint and Steel']], + ["We Need to Go Deeper", False, [], ['Progressive Tools']], + ["We Need to Go Deeper", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["We Need to Go Deeper", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["We Need to Go Deeper", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42026(self): + self.run_location_tests([ + ["Who's the Pillager Now?", False, []], + ["Who's the Pillager Now?", False, [], ['Archery']], + ["Who's the Pillager Now?", False, [], ['Progressive Resource Crafting']], + ["Who's the Pillager Now?", False, [], ['Progressive Tools']], + ["Who's the Pillager Now?", False, [], ['Progressive Weapons']], + ["Who's the Pillager Now?", True, ['Archery', 'Progressive Tools', 'Progressive Weapons', 'Progressive Resource Crafting']], + ]) + + def test_42027(self): + self.run_location_tests([ + ["Getting an Upgrade", False, []], + ["Getting an Upgrade", True, ["Progressive Tools"]], + ]) + + def test_42028(self): + self.run_location_tests([ + ["Tactical Fishing", False, []], + ["Tactical Fishing", False, [], ['Progressive Resource Crafting']], + ["Tactical Fishing", False, [], ['Progressive Tools']], + ["Tactical Fishing", False, [], ['Bucket']], + ["Tactical Fishing", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Bucket']], + ]) + + def test_42029(self): + self.run_location_tests([ + ["Zombie Doctor", False, []], + ["Zombie Doctor", False, [], ['Progressive Resource Crafting']], + ["Zombie Doctor", False, [], ['Flint and Steel']], + ["Zombie Doctor", False, [], ['Progressive Tools']], + ["Zombie Doctor", False, [], ['Progressive Weapons']], + ["Zombie Doctor", False, [], ['Progressive Armor', 'Shield']], + ["Zombie Doctor", False, [], ['Brewing']], + ["Zombie Doctor", False, [], ['Bottles']], + ["Zombie Doctor", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Zombie Doctor", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Zombie Doctor", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Zombie Doctor", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ["Zombie Doctor", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ]) + + def test_42030(self): + self.run_location_tests([ + ["The City at the End of the Game", False, []], + ["The City at the End of the Game", False, [], ['Progressive Resource Crafting']], + ["The City at the End of the Game", False, [], ['Flint and Steel']], + ["The City at the End of the Game", False, [], ['Progressive Tools']], + ["The City at the End of the Game", False, [], ['Progressive Weapons']], + ["The City at the End of the Game", False, [], ['Progressive Armor', 'Shield']], + ["The City at the End of the Game", False, [], ['Brewing']], + ["The City at the End of the Game", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["The City at the End of the Game", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["The City at the End of the Game", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The City at the End of the Game", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The City at the End of the Game", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The City at the End of the Game", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42031(self): + self.run_location_tests([ + ["Ice Bucket Challenge", False, []], + ["Ice Bucket Challenge", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools"]], + ["Ice Bucket Challenge", False, [], ["Progressive Resource Crafting"]], + ["Ice Bucket Challenge", True, ["Progressive Tools", "Progressive Tools", "Progressive Tools", "Progressive Resource Crafting"]], + ]) + + def test_42032(self): + self.run_location_tests([ + ["Remote Getaway", False, []], + ["Remote Getaway", False, [], ['Progressive Resource Crafting']], + ["Remote Getaway", False, [], ['Flint and Steel']], + ["Remote Getaway", False, [], ['Progressive Tools']], + ["Remote Getaway", False, [], ['Progressive Weapons']], + ["Remote Getaway", False, [], ['Progressive Armor', 'Shield']], + ["Remote Getaway", False, [], ['Brewing']], + ["Remote Getaway", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Remote Getaway", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Remote Getaway", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Remote Getaway", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Remote Getaway", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Remote Getaway", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42033(self): + self.run_location_tests([ + ["Into Fire", False, []], + ["Into Fire", False, [], ['Progressive Resource Crafting']], + ["Into Fire", False, [], ['Flint and Steel']], + ["Into Fire", False, [], ['Progressive Tools']], + ["Into Fire", False, [], ['Progressive Weapons']], + ["Into Fire", False, [], ['Progressive Armor', 'Shield']], + ["Into Fire", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Into Fire", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor']], + ["Into Fire", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor']], + ["Into Fire", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']], + ["Into Fire", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield']], + ]) + + def test_42034(self): + self.run_location_tests([ + ["War Pigs", False, []], + ["War Pigs", False, [], ['Progressive Resource Crafting']], + ["War Pigs", False, [], ['Flint and Steel']], + ["War Pigs", False, [], ['Progressive Tools']], + ["War Pigs", False, [], ['Progressive Weapons']], + ["War Pigs", False, [], ['Progressive Armor', 'Shield']], + ["War Pigs", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["War Pigs", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']], + ["War Pigs", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield']], + ]) + + def test_42035(self): + self.run_location_tests([ + ["Take Aim", False, []], + ["Take Aim", False, [], ['Archery']], + ["Take Aim", True, ['Archery']], + ]) + + def test_42036(self): + self.run_location_tests([ + ["Total Beelocation", False, []], + ["Total Beelocation", False, [], ['Enchanting']], + ["Total Beelocation", False, [], ['Silk Touch Book']], + ["Total Beelocation", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Total Beelocation", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Total Beelocation", True, ['Enchanting', 'Silk Touch Book', 'Progressive Resource Crafting', 'Progressive Resource Crafting', + 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42037(self): + self.run_location_tests([ + ["Arbalistic", False, []], + ["Arbalistic", False, [], ['Enchanting']], + ["Arbalistic", False, [], ['Piercing IV Book']], + ["Arbalistic", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Arbalistic", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Arbalistic", False, [], ['Archery']], + ["Arbalistic", True, ['Enchanting', 'Piercing IV Book', 'Progressive Resource Crafting', 'Progressive Resource Crafting', + 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Archery']], + ]) + + def test_42038(self): + self.run_location_tests([ + ["The End... Again...", False, []], + ["The End... Again...", False, [], ['Progressive Resource Crafting']], + ["The End... Again...", False, [], ['Flint and Steel']], + ["The End... Again...", False, [], ['Progressive Tools']], + ["The End... Again...", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["The End... Again...", False, [], ['Progressive Armor']], + ["The End... Again...", False, [], ['Brewing']], + ["The End... Again...", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["The End... Again...", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["The End... Again...", False, [], ['Archery']], + ["The End... Again...", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End... Again...", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42039(self): + self.run_location_tests([ + ["Acquire Hardware", False, []], + ["Acquire Hardware", False, [], ["Progressive Tools"]], + ["Acquire Hardware", False, [], ["Progressive Resource Crafting"]], + ["Acquire Hardware", True, ["Progressive Tools", "Progressive Resource Crafting"]], + ]) + + def test_42040(self): + self.run_location_tests([ + ["Not Quite \"Nine\" Lives", False, []], + ["Not Quite \"Nine\" Lives", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Not Quite \"Nine\" Lives", False, [], ['Flint and Steel']], + ["Not Quite \"Nine\" Lives", False, [], ['Progressive Tools']], + ["Not Quite \"Nine\" Lives", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Not Quite \"Nine\" Lives", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Not Quite \"Nine\" Lives", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42041(self): + self.run_location_tests([ + ["Cover Me with Diamonds", False, []], + ["Cover Me with Diamonds", False, ['Progressive Armor'], ['Progressive Armor']], + ["Cover Me with Diamonds", False, ['Progressive Tools'], ['Progressive Tools', 'Progressive Tools']], + ["Cover Me with Diamonds", False, [], ['Progressive Resource Crafting']], + ["Cover Me with Diamonds", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Armor', 'Progressive Armor']], + ]) + + def test_42042(self): + self.run_location_tests([ + ["Sky's the Limit", False, []], + ["Sky's the Limit", False, [], ['Progressive Resource Crafting']], + ["Sky's the Limit", False, [], ['Flint and Steel']], + ["Sky's the Limit", False, [], ['Progressive Tools']], + ["Sky's the Limit", False, [], ['Progressive Weapons']], + ["Sky's the Limit", False, [], ['Progressive Armor', 'Shield']], + ["Sky's the Limit", False, [], ['Brewing']], + ["Sky's the Limit", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Sky's the Limit", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Sky's the Limit", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Sky's the Limit", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Sky's the Limit", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Sky's the Limit", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42043(self): + self.run_location_tests([ + ["Hired Help", False, []], + ["Hired Help", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Hired Help", False, [], ['Progressive Tools']], + ["Hired Help", True, ['Progressive Tools', 'Progressive Resource Crafting', 'Progressive Resource Crafting']], + ]) + + def test_42044(self): + self.run_location_tests([ + ["Return to Sender", False, []], + ["Return to Sender", False, [], ['Progressive Resource Crafting']], + ["Return to Sender", False, [], ['Flint and Steel']], + ["Return to Sender", False, [], ['Progressive Tools']], + ["Return to Sender", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Return to Sender", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']], + ["Return to Sender", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42045(self): + self.run_location_tests([ + ["Sweet Dreams", False, []], + ["Sweet Dreams", True, ['Bed']], + ["Sweet Dreams", False, [], ['Bed', 'Progressive Weapons']], + ["Sweet Dreams", False, [], ['Bed', 'Progressive Resource Crafting', 'Campfire']], + ["Sweet Dreams", True, ['Progressive Weapons', 'Progressive Resource Crafting'], ['Bed', 'Campfire']], + ["Sweet Dreams", True, ['Progressive Weapons', 'Campfire'], ['Bed', 'Progressive Resource Crafting']], + ]) + + def test_42046(self): + self.run_location_tests([ + ["You Need a Mint", False, []], + ["You Need a Mint", False, [], ['Progressive Resource Crafting']], + ["You Need a Mint", False, [], ['Flint and Steel']], + ["You Need a Mint", False, [], ['Progressive Tools']], + ["You Need a Mint", False, [], ['Progressive Weapons']], + ["You Need a Mint", False, [], ['Progressive Armor', 'Shield']], + ["You Need a Mint", False, [], ['Brewing']], + ["You Need a Mint", False, [], ['Bottles']], + ["You Need a Mint", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["You Need a Mint", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ]) + + def test_42047(self): + self.run_location_tests([ + ["Adventure", True, []], + ]) + + def test_42048(self): + self.run_location_tests([ + ["Monsters Hunted", False, []], + ["Monsters Hunted", False, [], ['Progressive Resource Crafting']], + ["Monsters Hunted", False, [], ['Flint and Steel']], + ["Monsters Hunted", False, [], ['Progressive Tools']], + ["Monsters Hunted", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Monsters Hunted", False, [], ['Progressive Armor']], + ["Monsters Hunted", False, [], ['Shield']], + ["Monsters Hunted", False, [], ['Brewing']], + ["Monsters Hunted", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Monsters Hunted", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Monsters Hunted", False, [], ['Archery']], + ["Monsters Hunted", False, [], ['Enchanting']], + ["Monsters Hunted", False, [], ['Lead']], + ["Monsters Hunted", False, [], ['Bucket', 'Fishing Rod']], + ["Monsters Hunted", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', 'Lead', + 'Progressive Resource Crafting', 'Flint and Steel', 'Brewing', 'Bottles', + 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Enchanting', + 'Progressive Armor', 'Progressive Armor', 'Shield', 'Archery', 'Bucket', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["Monsters Hunted", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', 'Lead', + 'Progressive Resource Crafting', 'Flint and Steel', 'Brewing', 'Bottles', + 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Enchanting', + 'Progressive Armor', 'Progressive Armor', 'Shield', 'Archery', 'Fishing Rod', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42049(self): + self.run_location_tests([ + ["Enchanter", False, []], + ["Enchanter", False, [], ['Enchanting']], + ["Enchanter", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Enchanter", False, [], ['Progressive Resource Crafting']], + ["Enchanter", True, ['Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Enchanting', 'Progressive Resource Crafting']], + ]) + + def test_42050(self): + self.run_location_tests([ + ["Voluntary Exile", False, []], + ["Voluntary Exile", False, [], ['Progressive Weapons']], + ["Voluntary Exile", False, [], ['Progressive Armor', 'Shield']], + ["Voluntary Exile", False, [], ['Progressive Tools']], + ["Voluntary Exile", False, [], ['Progressive Resource Crafting']], + ["Voluntary Exile", True, ['Progressive Tools', 'Progressive Armor', 'Progressive Weapons', 'Progressive Resource Crafting']], + ["Voluntary Exile", True, ['Progressive Tools', 'Shield', 'Progressive Weapons', 'Progressive Resource Crafting']], + ]) + + def test_42051(self): + self.run_location_tests([ + ["Eye Spy", False, []], + ["Eye Spy", False, [], ['Progressive Resource Crafting']], + ["Eye Spy", False, [], ['Flint and Steel']], + ["Eye Spy", False, [], ['Progressive Tools']], + ["Eye Spy", False, [], ['Progressive Weapons']], + ["Eye Spy", False, [], ['Progressive Armor', 'Shield']], + ["Eye Spy", False, [], ['Brewing']], + ["Eye Spy", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Eye Spy", False, [], ['3 Ender Pearls']], + ["Eye Spy", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', '3 Ender Pearls']], + ["Eye Spy", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', '3 Ender Pearls']], + ["Eye Spy", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', '3 Ender Pearls']], + ["Eye Spy", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', '3 Ender Pearls']], + ]) + + def test_42052(self): + self.run_location_tests([ + ["The End", False, []], + ["The End", False, [], ['Progressive Resource Crafting']], + ["The End", False, [], ['Flint and Steel']], + ["The End", False, [], ['Progressive Tools']], + ["The End", False, [], ['Progressive Weapons']], + ["The End", False, [], ['Progressive Armor', 'Shield']], + ["The End", False, [], ['Brewing']], + ["The End", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["The End", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["The End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42053(self): + self.run_location_tests([ + ["Serious Dedication", False, []], + ["Serious Dedication", False, [], ['Progressive Resource Crafting']], + ["Serious Dedication", False, [], ['Flint and Steel']], + ["Serious Dedication", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Serious Dedication", False, [], ['Progressive Weapons']], + ["Serious Dedication", False, [], ['Progressive Armor', 'Shield']], + ["Serious Dedication", False, [], ['Brewing']], + ["Serious Dedication", False, [], ['Bottles']], + ["Serious Dedication", False, [], ['Bed']], + ["Serious Dedication", False, [], ['8 Netherite Scrap']], + ["Serious Dedication", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles', 'Bed', '8 Netherite Scrap']], + ["Serious Dedication", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles', 'Bed', '8 Netherite Scrap']], + ]) + + def test_42054(self): + self.run_location_tests([ + ["Postmortal", False, []], + ["Postmortal", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Postmortal", False, [], ['Progressive Armor']], + ["Postmortal", False, [], ['Shield']], + ["Postmortal", False, [], ['Progressive Resource Crafting']], + ["Postmortal", False, [], ['Progressive Tools']], + ["Postmortal", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Shield', 'Progressive Resource Crafting', 'Progressive Tools']], + ]) + + def test_42055(self): + self.run_location_tests([ + ["Monster Hunter", True, []], + ]) + + def test_42056(self): + self.run_location_tests([ + ["Adventuring Time", False, []], + ["Adventuring Time", False, [], ['Progressive Weapons']], + ["Adventuring Time", False, [], ['Progressive Resource Crafting']], + ["Adventuring Time", False, ['Progressive Tools'], ['Progressive Tools', 'Progressive Tools']], + ["Adventuring Time", True, ['Progressive Weapons', 'Progressive Resource Crafting', + 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42057(self): + self.run_location_tests([ + ["A Seedy Place", True, []], + ]) + + def test_42058(self): + self.run_location_tests([ + ["Those Were the Days", False, []], + ["Those Were the Days", False, [], ['Progressive Resource Crafting']], + ["Those Were the Days", False, [], ['Flint and Steel']], + ["Those Were the Days", False, [], ['Progressive Tools']], + ["Those Were the Days", False, [], ['Progressive Weapons']], + ["Those Were the Days", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Those Were the Days", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons']], + ["Those Were the Days", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], + ]) + + def test_42059(self): + self.run_location_tests([ + ["Hero of the Village", False, []], + ["Hero of the Village", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Hero of the Village", False, [], ['Progressive Armor']], + ["Hero of the Village", False, [], ['Shield']], + ["Hero of the Village", False, [], ['Progressive Resource Crafting']], + ["Hero of the Village", False, [], ['Progressive Tools']], + ["Hero of the Village", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Shield', 'Progressive Resource Crafting', 'Progressive Tools']], + ]) + + def test_42060(self): + self.run_location_tests([ + ["Hidden in the Depths", False, []], + ["Hidden in the Depths", False, [], ['Progressive Resource Crafting']], + ["Hidden in the Depths", False, [], ['Flint and Steel']], + ["Hidden in the Depths", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Hidden in the Depths", False, [], ['Progressive Weapons']], + ["Hidden in the Depths", False, [], ['Progressive Armor', 'Shield']], + ["Hidden in the Depths", False, [], ['Brewing']], + ["Hidden in the Depths", False, [], ['Bottles']], + ["Hidden in the Depths", False, [], ['Bed']], + ["Hidden in the Depths", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles', 'Bed']], + ["Hidden in the Depths", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles', 'Bed']], + ]) + + def test_42061(self): + self.run_location_tests([ + ["Beaconator", False, []], + ["Beaconator", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Beaconator", False, [], ['Flint and Steel']], + ["Beaconator", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Beaconator", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Beaconator", False, ['Progressive Armor'], ['Progressive Armor']], + ["Beaconator", False, [], ['Brewing']], + ["Beaconator", False, [], ['Bottles']], + ["Beaconator", False, [], ['Enchanting']], + ["Beaconator", True, [], ['Bucket']], + ["Beaconator", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', + 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Progressive Armor', + 'Brewing', 'Bottles', 'Enchanting']], + ]) + + def test_42062(self): + self.run_location_tests([ + ["Withering Heights", False, []], + ["Withering Heights", False, [], ['Progressive Resource Crafting']], + ["Withering Heights", False, [], ['Flint and Steel']], + ["Withering Heights", False, [], ['Progressive Tools']], + ["Withering Heights", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], + ["Withering Heights", False, ['Progressive Armor'], ['Progressive Armor']], + ["Withering Heights", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Withering Heights", False, [], ['Brewing']], + ["Withering Heights", False, [], ['Bottles']], + ["Withering Heights", False, [], ['Enchanting']], + ["Withering Heights", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Progressive Armor', + 'Brewing', 'Bottles', 'Enchanting']], + ]) + + def test_42063(self): + self.run_location_tests([ + ["A Balanced Diet", False, []], + ["A Balanced Diet", False, [], ['Fishing Rod']], + ["A Balanced Diet", False, [], ['Campfire']], + ["A Balanced Diet", False, [], ['Bottles']], + ["A Balanced Diet", False, [], ['Progressive Resource Crafting']], + ["A Balanced Diet", False, [], ['Flint and Steel']], + ["A Balanced Diet", False, [], ['Progressive Weapons']], + ["A Balanced Diet", False, [], ['Progressive Armor', 'Shield']], + ["A Balanced Diet", False, [], ['Brewing']], + ["A Balanced Diet", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["A Balanced Diet", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["A Balanced Diet", True, ['Progressive Resource Crafting', 'Bottles', 'Campfire', 'Fishing Rod', + 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', + 'Flint and Steel', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', + 'Progressive Tools', 'Progressive Armor']], + ["A Balanced Diet", True, ['Progressive Resource Crafting', 'Bottles', 'Campfire', 'Fishing Rod', + 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', + 'Flint and Steel', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', + 'Bucket', 'Progressive Armor']], + ["A Balanced Diet", True, ['Progressive Resource Crafting', 'Bottles', 'Campfire', 'Fishing Rod', + 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', + 'Flint and Steel', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', + 'Progressive Tools', 'Shield']], + ["A Balanced Diet", True, ['Progressive Resource Crafting', 'Bottles', 'Campfire', 'Fishing Rod', + 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', + 'Flint and Steel', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', + 'Bucket', 'Shield']], + ]) + + def test_42064(self): + self.run_location_tests([ + ["Subspace Bubble", False, []], + ["Subspace Bubble", False, [], ['Progressive Resource Crafting']], + ["Subspace Bubble", False, [], ['Flint and Steel']], + ["Subspace Bubble", False, [], ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Subspace Bubble", True, ['Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Resource Crafting']], + ]) + + def test_42065(self): + self.run_location_tests([ + ["Husbandry", True, []], + ]) + + def test_42066(self): + self.run_location_tests([ + ["Country Lode, Take Me Home", False, []], + ["Country Lode, Take Me Home", False, [], ['Progressive Resource Crafting']], + ["Country Lode, Take Me Home", False, [], ['Progressive Tools', 'Progressive Tools']], + ["Country Lode, Take Me Home", True, ['Progressive Tools', 'Progressive Tools', 'Progressive Resource Crafting']], + ]) + + def test_42067(self): + self.run_location_tests([ + ["Bee Our Guest", False, []], + ["Bee Our Guest", False, [], ['Campfire']], + ["Bee Our Guest", False, [], ['Bottles']], + ["Bee Our Guest", False, [], ['Progressive Resource Crafting']], + ["Bee Our Guest", True, ['Campfire', 'Bottles', 'Progressive Resource Crafting']], + ]) + + def test_42068(self): + self.run_location_tests([ + ["What a Deal!", False, []], + ["What a Deal!", False, [], ['Progressive Weapons']], + ["What a Deal!", False, [], ['Campfire', 'Progressive Resource Crafting']], + ["What a Deal!", True, ['Progressive Weapons', 'Campfire']], + ["What a Deal!", True, ['Progressive Weapons', 'Progressive Resource Crafting']], + ]) + + def test_42069(self): + self.run_location_tests([ + ["Uneasy Alliance", False, []], + ["Uneasy Alliance", False, [], ['Progressive Resource Crafting']], + ["Uneasy Alliance", False, [], ['Flint and Steel']], + ["Uneasy Alliance", False, [], ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Uneasy Alliance", False, [], ['Fishing Rod']], + ["Uneasy Alliance", True, ['Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Resource Crafting', 'Fishing Rod']], + ]) + + def test_42070(self): + self.run_location_tests([ + ["Diamonds!", False, []], + ["Diamonds!", True, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools"]], + ["Diamonds!", False, [], ["Progressive Tools", "Progressive Tools"]], + ["Diamonds!", False, [], ["Progressive Resource Crafting"]], + ["Diamonds!", False, ["Progressive Tools", "Progressive Resource Crafting"]], + ["Diamonds!", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting"]], + ]) + + def test_42071(self): + self.run_location_tests([ + ["A Terrible Fortress", False, []], + ["A Terrible Fortress", False, [], ['Progressive Resource Crafting']], + ["A Terrible Fortress", False, [], ['Flint and Steel']], + ["A Terrible Fortress", False, [], ['Progressive Tools']], + ["A Terrible Fortress", False, [], ['Progressive Weapons']], + ["A Terrible Fortress", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["A Terrible Fortress", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons']], + ["A Terrible Fortress", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], + ]) + + def test_42072(self): + self.run_location_tests([ + ["A Throwaway Joke", False, []], + ["A Throwaway Joke", False, [], ['Progressive Weapons']], + ["A Throwaway Joke", True, ['Progressive Weapons', 'Progressive Armor', 'Progressive Tools', 'Progressive Resource Crafting']], + ]) + + def test_42073(self): + self.run_location_tests([ + ["Minecraft", True, []], + ]) + + def test_42074(self): + self.run_location_tests([ + ["Sticky Situation", False, []], + ["Sticky Situation", False, [], ['Bottles']], + ["Sticky Situation", False, [], ['Progressive Resource Crafting']], + ["Sticky Situation", False, [], ['Campfire']], + ["Sticky Situation", True, ['Bottles', 'Progressive Resource Crafting', 'Campfire']], + ]) + + def test_42075(self): + self.run_location_tests([ + ["Ol' Betsy", False, []], + ["Ol' Betsy", False, [], ['Archery']], + ["Ol' Betsy", False, [], ['Progressive Resource Crafting']], + ["Ol' Betsy", False, [], ['Progressive Tools']], + ["Ol' Betsy", True, ['Archery', 'Progressive Resource Crafting', 'Progressive Tools']], + ]) + + def test_42076(self): + self.run_location_tests([ + ["Cover Me in Debris", False, []], + ["Cover Me in Debris", False, [], ['Progressive Resource Crafting']], + ["Cover Me in Debris", False, [], ['Flint and Steel']], + ["Cover Me in Debris", False, ['Progressive Tools', 'Progressive Tools'], ['Progressive Tools']], + ["Cover Me in Debris", False, [], ['Progressive Weapons']], + ["Cover Me in Debris", False, ['Progressive Armor'], ['Progressive Armor']], + ["Cover Me in Debris", False, [], ['Brewing']], + ["Cover Me in Debris", False, [], ['Bottles']], + ["Cover Me in Debris", False, [], ['Bed']], + ["Cover Me in Debris", False, ['8 Netherite Scrap'], ['8 Netherite Scrap']], + ["Cover Me in Debris", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Progressive Armor', + 'Brewing', 'Bottles', 'Bed', '8 Netherite Scrap', '8 Netherite Scrap']], + ]) + + def test_42077(self): + self.run_location_tests([ + ["The End?", False, []], + ["The End?", False, [], ['Progressive Resource Crafting']], + ["The End?", False, [], ['Flint and Steel']], + ["The End?", False, [], ['Progressive Tools']], + ["The End?", False, [], ['Progressive Weapons']], + ["The End?", False, [], ['Progressive Armor', 'Shield']], + ["The End?", False, [], ['Brewing']], + ["The End?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["The End?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["The End?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ["The End?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def test_42078(self): + self.run_location_tests([ + ["The Parrots and the Bats", True, []], + ]) + + def test_42079(self): + self.run_location_tests([ + ["A Complete Catalogue", False, []], + ["A Complete Catalogue", False, [], ['Progressive Weapons']], + ["A Complete Catalogue", False, [], ['Campfire', 'Progressive Resource Crafting']], + ["A Complete Catalogue", True, ['Progressive Weapons', 'Campfire']], + ["A Complete Catalogue", True, ['Progressive Weapons', 'Progressive Resource Crafting']], + ]) + + def test_42080(self): + self.run_location_tests([ + ["Getting Wood", True, []], + ]) + + def test_42081(self): + self.run_location_tests([ + ["Time to Mine!", True, []], + ]) + + def test_42082(self): + self.run_location_tests([ + ["Hot Topic", False, []], + ["Hot Topic", True, ['Progressive Resource Crafting']], + ]) + + def test_42083(self): + self.run_location_tests([ + ["Bake Bread", True, []], + ]) + + def test_42084(self): + self.run_location_tests([ + ["The Lie", False, []], + ["The Lie", False, [], ['Progressive Resource Crafting']], + ["The Lie", False, [], ['Bucket']], + ["The Lie", False, [], ['Progressive Tools']], + ["The Lie", True, ['Bucket', 'Progressive Resource Crafting', 'Progressive Tools']], + ]) + + def test_42085(self): + self.run_location_tests([ + ["On a Rail", False, []], + ["On a Rail", False, [], ['Progressive Resource Crafting']], + ["On a Rail", False, ['Progressive Tools'], ['Progressive Tools', 'Progressive Tools']], + ["On a Rail", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools']], + ]) + + def test_42086(self): + self.run_location_tests([ + ["Time to Strike!", True, []], + ]) + + def test_42087(self): + self.run_location_tests([ + ["Cow Tipper", True, []], + ]) + + def test_42088(self): + self.run_location_tests([ + ["When Pigs Fly", False, []], + ["When Pigs Fly", False, [], ['Progressive Resource Crafting']], + ["When Pigs Fly", False, [], ['Progressive Tools']], + ["When Pigs Fly", False, [], ['Progressive Weapons']], + ["When Pigs Fly", False, [], ['Fishing Rod']], + ["When Pigs Fly", False, [], ['Saddle']], + ["When Pigs Fly", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', + 'Fishing Rod', 'Progressive Weapons']], + ]) + + def test_42089(self): + self.run_location_tests([ + ["Overkill", False, []], + ["Overkill", False, [], ['Progressive Resource Crafting']], + ["Overkill", False, [], ['Flint and Steel']], + ["Overkill", False, [], ['Progressive Tools']], + ["Overkill", False, [], ['Progressive Weapons']], + ["Overkill", False, [], ['Progressive Armor', 'Shield']], + ["Overkill", False, [], ['Brewing']], + ["Overkill", False, [], ['Bottles']], + ["Overkill", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Overkill", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Overkill", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', 'Bottles']], + ["Overkill", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ["Overkill", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', 'Bottles']], + ]) + + def test_42090(self): + self.run_location_tests([ + ["Librarian", False, []], + ["Librarian", True, ['Enchanting']], + ]) + + def test_42091(self): + self.run_location_tests([ + ["Overpowered", False, []], + ["Overpowered", False, [], ['Progressive Resource Crafting']], + ["Overpowered", False, [], ['Flint and Steel']], + ["Overpowered", False, ['Progressive Tools', 'Progressive Tools', 'Bucket', 'Flint and Steel']], + ["Overpowered", False, [], ['Progressive Weapons']], + ["Overpowered", False, [], ['Progressive Armor', 'Shield']], + ["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor']], + ["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor']], + ["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Weapons', 'Shield']], + ["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']], + ]) + + def test_42092(self): + self.run_location_tests([ + ["Wax On", False, []], + ["Wax On", False, [], ["Progressive Tools"]], + ["Wax On", False, [], ["Progressive Resource Crafting"]], + ["Wax On", False, [], ["Campfire"]], + ["Wax On", True, ["Progressive Resource Crafting", "Progressive Tools", "Campfire"]], + ]) + + def test_42093(self): + self.run_location_tests([ + ["Wax Off", False, []], + ["Wax Off", False, [], ["Progressive Tools"]], + ["Wax Off", False, [], ["Campfire", "Progressive Weapons"]], + ["Wax Off", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["Wax Off", False, [], ["Progressive Weapons", "Progressive Resource Crafting"]], + ["Wax Off", True, ["Progressive Tools", "Progressive Resource Crafting", "Campfire"]], + ["Wax Off", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ["Wax Off", True, ["Progressive Tools", "Campfire", "Progressive Weapons"]] + ]) + + def test_42094(self): + self.run_location_tests([ + ["The Cutest Predator", False, []], + ["The Cutest Predator", False, [], ["Progressive Weapons"]], + ["The Cutest Predator", False, [], ["Progressive Tools"]], + ["The Cutest Predator", False, [], ["Progressive Resource Crafting"]], + ["The Cutest Predator", False, [], ["Bucket"]], + ["The Cutest Predator", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42095(self): + self.run_location_tests([ + ["The Healing Power of Friendship", False, []], + ["The Healing Power of Friendship", False, [], ["Progressive Weapons"]], + ["The Healing Power of Friendship", False, [], ["Progressive Tools"]], + ["The Healing Power of Friendship", False, [], ["Progressive Resource Crafting"]], + ["The Healing Power of Friendship", False, [], ["Bucket"]], + ["The Healing Power of Friendship", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42096(self): + self.run_location_tests([ + ["Is It a Bird?", False, []], + ["Is It a Bird?", False, [], ["Progressive Weapons"]], + ["Is It a Bird?", False, [], ["Progressive Tools"]], + ["Is It a Bird?", False, [], ["Progressive Resource Crafting"]], + ["Is It a Bird?", False, [], ["Spyglass"]], + ["Is It a Bird?", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Spyglass"]], + ]) + + def test_42097(self): + self.run_location_tests([ + ["Is It a Balloon?", False, []], + ["Is It a Balloon?", False, [], ['Progressive Resource Crafting']], + ["Is It a Balloon?", False, [], ['Flint and Steel']], + ["Is It a Balloon?", False, [], ['Progressive Tools']], + ["Is It a Balloon?", False, [], ['Progressive Weapons']], + ["Is It a Balloon?", False, [], ['Spyglass']], + ["Is It a Balloon?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Spyglass']], + ["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Spyglass']], + ]) + + def test_42098(self): + self.run_location_tests([ + ["Is It a Plane?", False, []], + ["Is It a Plane?", False, [], ['Progressive Resource Crafting']], + ["Is It a Plane?", False, [], ['Flint and Steel']], + ["Is It a Plane?", False, [], ['Progressive Tools']], + ["Is It a Plane?", False, [], ['Progressive Weapons']], + ["Is It a Plane?", False, [], ['Progressive Armor', 'Shield']], + ["Is It a Plane?", False, [], ['Brewing']], + ["Is It a Plane?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Is It a Plane?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Is It a Plane?", False, [], ['Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ]) + + def test_42099(self): + self.run_location_tests([ + ["Surge Protector", False, []], + ["Surge Protector", False, [], ['Channeling Book']], + ["Surge Protector", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Surge Protector", False, [], ['Enchanting']], + ["Surge Protector", False, [], ['Progressive Tools']], + ["Surge Protector", False, [], ['Progressive Weapons']], + ["Surge Protector", True, ['Progressive Weapons', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Enchanting', 'Progressive Resource Crafting', 'Progressive Resource Crafting', 'Channeling Book']], + ]) + + def test_42100(self): + self.run_location_tests([ + ["Light as a Rabbit", False, []], + ["Light as a Rabbit", False, [], ["Progressive Weapons"]], + ["Light as a Rabbit", False, [], ["Progressive Tools"]], + ["Light as a Rabbit", False, [], ["Progressive Resource Crafting"]], + ["Light as a Rabbit", False, [], ["Bucket"]], + ["Light as a Rabbit", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42101(self): + self.run_location_tests([ + ["Glow and Behold!", False, []], + ["Glow and Behold!", False, [], ["Progressive Weapons"]], + ["Glow and Behold!", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Glow and Behold!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ["Glow and Behold!", True, ["Progressive Weapons", "Campfire"]], + ]) + + def test_42102(self): + self.run_location_tests([ + ["Whatever Floats Your Goat!", False, []], + ["Whatever Floats Your Goat!", False, [], ["Progressive Weapons"]], + ["Whatever Floats Your Goat!", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]], + ]) + + # bucket, iron pick + def test_42103(self): + self.run_location_tests([ + ["Caves & Cliffs", False, []], + ["Caves & Cliffs", False, [], ["Bucket"]], + ["Caves & Cliffs", False, [], ["Progressive Tools"]], + ["Caves & Cliffs", False, [], ["Progressive Resource Crafting"]], + ["Caves & Cliffs", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Bucket"]], + ]) + + # bucket, fishing rod, saddle + def test_42104(self): + self.run_location_tests([ + ["Feels Like Home", False, []], + ["Feels Like Home", False, [], ['Progressive Resource Crafting']], + ["Feels Like Home", False, [], ['Progressive Tools']], + ["Feels Like Home", False, [], ['Fishing Rod']], + ["Feels Like Home", False, [], ['Saddle']], + ["Feels Like Home", False, [], ['Bucket']], + ["Feels Like Home", False, [], ['Flint and Steel']], + ["Feels Like Home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Fishing Rod']], + ]) + + # iron pick, combat + def test_42105(self): + self.run_location_tests([ + ["Sound of Music", False, []], + ["Sound of Music", False, [], ["Progressive Tools"]], + ["Sound of Music", False, [], ["Progressive Resource Crafting"]], + ["Sound of Music", False, [], ["Progressive Weapons"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ]) + + # bucket, nether, villager + def test_42106(self): + self.run_location_tests([ + ["Star Trader", False, []], + ["Star Trader", False, [], ["Bucket"]], + ["Star Trader", False, [], ["Flint and Steel"]], + ["Star Trader", False, [], ["Progressive Tools"]], + ["Star Trader", False, [], ["Progressive Resource Crafting"]], + ["Star Trader", False, [], ["Progressive Weapons"]], + ["Star Trader", True, ["Bucket", "Flint and Steel", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ]) + + # bucket, redstone -> iron pick, pillager outpost -> adventure + def test_42107(self): + self.run_location_tests([ + ["Birthday Song", False, []], + ["Birthday Song", False, [], ["Bucket"]], + ["Birthday Song", False, [], ["Progressive Tools"]], + ["Birthday Song", False, [], ["Progressive Weapons"]], + ["Birthday Song", False, [], ["Progressive Resource Crafting"]], + ["Birthday Song", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Progressive Weapons", "Bucket"]], + ]) + + # bucket, adventure + def test_42108(self): + self.run_location_tests([ + ["Bukkit Bukkit", False, []], + ["Bukkit Bukkit", False, [], ["Bucket"]], + ["Bukkit Bukkit", False, [], ["Progressive Tools"]], + ["Bukkit Bukkit", False, [], ["Progressive Weapons"]], + ["Bukkit Bukkit", False, [], ["Progressive Resource Crafting"]], + ["Bukkit Bukkit", True, ["Bucket", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42109(self): + self.run_location_tests([ + ["It Spreads", False, []], + ["It Spreads", False, [], ["Progressive Tools"]], + ["It Spreads", False, [], ["Progressive Weapons"]], + ["It Spreads", False, [], ["Progressive Resource Crafting"]], + ["It Spreads", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42110(self): + self.run_location_tests([ + ["Sneak 100", False, []], + ["Sneak 100", False, [], ["Progressive Tools"]], + ["Sneak 100", False, [], ["Progressive Weapons"]], + ["Sneak 100", False, [], ["Progressive Resource Crafting"]], + ["Sneak 100", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # adventure, lead, bucket + def test_42111(self): + self.run_location_tests([ + ["When the Squad Hops into Town", False, []], + ["When the Squad Hops into Town", False, [], ["Progressive Weapons"]], + ["When the Squad Hops into Town", False, [], ["Progressive Resource Crafting"]], + ["When the Squad Hops into Town", False, [], ["Progressive Tools"]], + ["When the Squad Hops into Town", False, [], ["Bucket"]], + ["When the Squad Hops into Town", False, [], ["Lead"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Progressive Resource Crafting", + "Progressive Tools", "Bucket"]], + ]) + + # adventure, lead, bucket, nether + def test_42112(self): + self.run_location_tests([ + ["With Our Powers Combined!", False, []], + ["With Our Powers Combined!", False, [], ["Lead"]], + ["With Our Powers Combined!", False, [], ["Bucket"]], + ["With Our Powers Combined!", False, [], ["Progressive Tools"]], + ["With Our Powers Combined!", False, [], ["Flint and Steel"]], + ["With Our Powers Combined!", False, [], ["Progressive Weapons"]], + ["With Our Powers Combined!", False, [], ["Progressive Resource Crafting"]], + ["With Our Powers Combined!", True, ["Progressive Weapons", "Lead", "Progressive Resource Crafting", + "Flint and Steel", "Progressive Tools", "Bucket"]], + ]) + + # pillager outpost -> adventure + def test_42113(self): + self.run_location_tests([ + ["You've Got a Friend in Me", False, []], + ["You've Got a Friend in Me", False, [], ["Progressive Weapons"]], + ["You've Got a Friend in Me", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Campfire"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # ocean ruins, brush + def test_42114(self): + self.run_location_tests([ + ["Smells Interesting", False, []], + ["Smells Interesting", False, [], ["Brush"]], + ["Smells Interesting", False, [], ["Progressive Weapons"]], + ["Smells Interesting", False, [], ["Progressive Resource Crafting"]], + ["Smells Interesting", False, [], ["Progressive Tools"]], + ["Smells Interesting", True, ["Brush", "Progressive Weapons", + "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + # ocean ruins, brush + def test_42115(self): + self.run_location_tests([ + ["Little Sniffs", False, []], + ["Little Sniffs", False, [], ["Brush"]], + ["Little Sniffs", False, [], ["Progressive Weapons"]], + ["Little Sniffs", False, [], ["Progressive Resource Crafting"]], + ["Little Sniffs", False, [], ["Progressive Tools"]], + ["Little Sniffs", True, ["Brush", "Progressive Weapons", + "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + # ocean ruins, brush + def test_42116(self): + self.run_location_tests([ + ["Planting the Past", False, []], + ["Planting the Past", False, [], ["Brush"]], + ["Planting the Past", False, [], ["Progressive Weapons"]], + ["Planting the Past", False, [], ["Progressive Resource Crafting"]], + ["Planting the Past", False, [], ["Progressive Tools"]], + ["Planting the Past", True, ["Brush", "Progressive Weapons", + "Progressive Resource Crafting", "Progressive Tools"]], + ]) + + def test_42117(self): + self.run_location_tests([ + ["Crafting a New Look", False, []], + ["Crafting a New Look", False, [], ["Progressive Tools"]], + ["Crafting a New Look", False, [], ["Progressive Resource Crafting"]], + ["Crafting a New Look", False, [], ["Progressive Weapons"]], + ["Crafting a New Look", False, ["Progressive Tools"], ["Progressive Tools", "Progressive Tools", + "Progressive Armor", "Shield", "Brush"]], + ["Crafting a New Look", True, ["Progressive Resource Crafting", "Progressive Weapons", + "Progressive Tools", "Progressive Armor"], []], + ["Crafting a New Look", True, ["Progressive Resource Crafting", "Progressive Weapons", + "Progressive Tools", "Shield"], []], + ["Crafting a New Look", True, ["Progressive Resource Crafting", "Progressive Weapons", + "Progressive Tools", "Progressive Tools"], []], + ["Crafting a New Look", True, ["Progressive Resource Crafting", "Progressive Weapons", + "Progressive Tools", "Brush"], []], + ]) + + def test_42118(self): + self.run_location_tests([ + ["Smithing with Style", False, []], + ["Smithing with Style", False, [], ["Progressive Resource Crafting"]], + ["Smithing with Style", False, [], ["Brush"]], + ["Smithing with Style", False, [], ["Progressive Weapons"]], + ["Smithing with Style", False, ["3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls"], ["3 Ender Pearls"]], + ["Smithing with Style", False, [], ["Brewing"]], + ["Smithing with Style", False, [], ["Flint and Steel"]], + ["Smithing with Style", False, ["Progressive Tools"], ["Progressive Tools", "Progressive Tools"]], + ["Smithing with Style", False, [], ["Progressive Armor", "Shield"]], + ["Smithing with Style", False, ["Progressive Tools", "Progressive Tools"], ["Bucket", "Progressive Tools"]], + ["Smithing with Style", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools", + "Fishing Rod"]], + ["Smithing with Style", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools", "Bucket"]], + ["Smithing with Style", False, [], ["Bucket", "Fishing Rod"]], + ["Smithing with Style", False, [], ["Bucket", "Bottles"]], + ["Smithing with Style", False, [], ["Fishing Rod", "Enchanting"]], + ["Smithing with Style", False, [], ["Bottles", "Enchanting"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Progressive Armor", "Bucket", "Bottles", "Fishing Rod"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Shield", "Bucket", "Bottles", "Fishing Rod"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Progressive Armor", "Progressive Tools", + "Bottles", "Fishing Rod"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Shield", "Bottles", "Fishing Rod"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Progressive Armor", "Bucket", "Enchanting"]], + ["Smithing with Style", True, ["Progressive Tools", "Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting", "Brush", "Progressive Weapons", "Brewing", + "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", "3 Ender Pearls", + "Flint and Steel", "Shield", "Bucket", "Enchanting"]], + ]) + + def test_42119(self): + self.run_location_tests([ + ["Respecting the Remnants", False, []], + ["Respecting the Remnants", False, [], ["Progressive Tools"]], + ["Respecting the Remnants", False, [], ["Progressive Resource Crafting"]], + ["Respecting the Remnants", False, [], ["Brush"]], + ["Respecting the Remnants", False, [], ["Progressive Weapons"]], + ["Respecting the Remnants", True, ["Progressive Tools", "Progressive Resource Crafting", + "Brush", "Progressive Weapons"]], + ]) + + def test_42120(self): + self.run_location_tests([ + ["Careful Restoration", False, []], + ["Careful Restoration", False, [], ["Progressive Tools"]], + ["Careful Restoration", False, [], ["Progressive Resource Crafting"]], + ["Careful Restoration", False, [], ["Brush"]], + ["Careful Restoration", False, [], ["Progressive Weapons"]], + ["Careful Restoration", True, ["Progressive Tools", "Progressive Resource Crafting", + "Brush", "Progressive Weapons"]], + ]) + + def test_42121(self): + self.run_location_tests([ + ["The Power of Books", False, []], + ["The Power of Books", False, [], ["Progressive Resource Crafting"]], + ["The Power of Books", False, [], ["Flint and Steel"]], + ["The Power of Books", False, ["Progressive Tools"], ["Progressive Tools", "Progressive Tools"]], + ["The Power of Books", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools", "Bucket"]], + ["The Power of Books", True, ["Progressive Resource Crafting", "Flint and Steel", + "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ["The Power of Books", True, ["Progressive Resource Crafting", "Flint and Steel", + "Progressive Tools", "Progressive Tools", "Bucket"]], + ]) + + def test_42122(self): + self.run_location_tests([ + ["Isn't It Scute?", False, []], + ["Isn't It Scute?", False, [], ["Progressive Weapons"]], + ["Isn't It Scute?", False, [], ["Progressive Resource Crafting"]], + ["Isn't It Scute?", False, [], ["Progressive Tools"]], + ["Isn't It Scute?", False, [], ["Brush"]], + ["Isn't It Scute?", True, ["Progressive Weapons", "Progressive Resource Crafting", + "Progressive Tools", "Brush"]], + ]) + + def test_42123(self): + self.run_location_tests([ + ["Shear Brilliance", False, []], + ["Shear Brilliance", False, [], ["Progressive Weapons"]], + ["Shear Brilliance", False, [], ["Progressive Resource Crafting"]], + ["Shear Brilliance", False, [], ["Progressive Tools"]], + ["Shear Brilliance", False, [], ["Brush"]], + ["Shear Brilliance", True, ["Progressive Weapons", "Progressive Resource Crafting", + "Progressive Tools", "Brush"]], + ]) + + def test_42124(self): + self.run_location_tests([ + ["Good as New", False, []], + ["Good as New", False, [], ["Progressive Weapons"]], + ["Good as New", False, [], ["Progressive Resource Crafting"]], + ["Good as New", False, [], ["Progressive Tools"]], + ["Good as New", False, [], ["Brush"]], + ["Good as New", True, ["Progressive Weapons", "Progressive Resource Crafting", + "Progressive Tools", "Brush"]], + ]) + + def test_42125(self): + self.run_location_tests([ + ["The Whole Pack", False, []], + ["The Whole Pack", False, [], ["Progressive Weapons"]], + ["The Whole Pack", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["The Whole Pack", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ["The Whole Pack", True, ["Progressive Weapons", "Campfire"]], + ]) + + def test_42126(self): + self.run_location_tests([ + ["Minecraft: Trial(s) Edition", False, []], + ["Minecraft: Trial(s) Edition", False, [], ["Progressive Weapons"]], + ["Minecraft: Trial(s) Edition", False, [], ["Progressive Tools"]], + ["Minecraft: Trial(s) Edition", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Minecraft: Trial(s) Edition", True, ["Progressive Tools", "Progressive Weapons", + "Progressive Resource Crafting"]], + ["Minecraft: Trial(s) Edition", True, ["Progressive Tools", "Progressive Weapons", + "Campfire"]], + ]) + + def test_42127(self): + self.run_location_tests([ + ["Under Lock and Key", False, []], + ["Under Lock and Key", False, [], ["Progressive Weapons"]], + ["Under Lock and Key", False, [], ["Progressive Resource Crafting"]], + ["Under Lock and Key", False, [], ["Progressive Tools"]], + ["Under Lock and Key", False, [], ["Progressive Armor", "Shield"]], + ["Under Lock and Key", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Progressive Armor"]], + ["Under Lock and Key", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Shield"]], + ]) + + def test_42128(self): + self.run_location_tests([ + ["Blowback", False, []], + ["Blowback", False, [], ["Progressive Weapons"]], + ["Blowback", False, [], ["Progressive Resource Crafting"]], + ["Blowback", False, [], ["Progressive Tools"]], + ["Blowback", False, [], ["Progressive Armor", "Shield"]], + ["Blowback", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Progressive Armor"]], + ["Blowback", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Shield"]], + ]) + + def test_42129(self): + self.run_location_tests([ + ["Who Needs Rockets?", False, []], + ["Who Needs Rockets?", False, [], ["Progressive Weapons"]], + ["Who Needs Rockets?", False, [], ["Progressive Resource Crafting"]], + ["Who Needs Rockets?", False, [], ["Progressive Tools"]], + ["Who Needs Rockets?", False, [], ["Progressive Armor", "Shield"]], + ["Who Needs Rockets?", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Progressive Armor"]], + ["Who Needs Rockets?", True, ["Progressive Weapons", "Progressive Tools", + "Progressive Resource Crafting", "Shield"]], + ]) + + def test_42130(self): + self.run_location_tests([ + ["Crafters Crafting Crafters", False, []], + ["Crafters Crafting Crafters", False, [], ["Progressive Resource Crafting"]], + ["Crafters Crafting Crafters", False, ["Progressive Tools"], ["Progressive Tools", "Progressive Tools"]], + ["Crafters Crafting Crafters", True, ["Progressive Tools", "Progressive Tools", + "Progressive Resource Crafting"]], + ]) + + def test_42131(self): + self.run_location_tests([ + ["Lighten Up", False, []], + ["Lighten Up", False, [], ["Progressive Weapons"]], + ["Lighten Up", False, [], ["Progressive Tools"]], + ["Lighten Up", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Lighten Up", True, ["Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ["Lighten Up", True, ["Progressive Tools", "Progressive Weapons", "Campfire"]], + ]) + + def test_42132(self): + self.run_location_tests([ + ["Over-Overkill", False, []], + ["Over-Overkill", False, [], ["Progressive Resource Crafting"]], + ["Over-Overkill", False, [], ["Progressive Tools"]], + ["Over-Overkill", False, ["Progressive Weapons"], ["Progressive Weapons", "Progressive Weapons"]], + ["Over-Overkill", False, [], ["Progressive Armor"]], + ["Over-Overkill", False, [], ["Shield"]], + ["Over-Overkill", True, ["Progressive Resource Crafting", "Progressive Tools", + "Progressive Weapons", "Progressive Weapons", "Progressive Armor", "Shield"]], + ]) + + def test_42133(self): + self.run_location_tests([ + ["Revaulting", False, []], + ["Revaulting", False, [], ["Progressive Resource Crafting"]], + ["Revaulting", False, [], ["Progressive Tools"]], + ["Revaulting", False, ["Progressive Weapons"], ["Progressive Weapons", "Progressive Weapons"]], + ["Revaulting", False, [], ["Progressive Armor"]], + ["Revaulting", False, [], ["Shield"]], + ["Revaulting", True, ["Progressive Resource Crafting", "Progressive Tools", + "Progressive Weapons", "Progressive Weapons", "Progressive Armor", "Shield"]], + ]) + + def test_42134(self): + self.run_location_tests([ + ["Stay Hydrated!", False, []], + ["Stay Hydrated!", False, [], ["Progressive Resource Crafting"]], + ["Stay Hydrated!", False, [], ["Progressive Tools"]], + ["Stay Hydrated!", False, [], ["Flint and Steel"]], + ["Stay Hydrated!", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools", "Bucket"]], + ["Stay Hydrated!", True, ["Progressive Resource Crafting", "Flint and Steel", + "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ["Stay Hydrated!", True, ["Progressive Resource Crafting", "Flint and Steel", + "Progressive Tools", "Bucket"]], + ]) + + def test_42135(self): + self.run_location_tests([ + ["Heart Transplanter", False, []], + ["Heart Transplanter", False, [], ["Progressive Weapons"]], + ["Heart Transplanter", False, [], ["Progressive Tools"]], + ["Heart Transplanter", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]], + ["Heart Transplanter", False, [], ["Progressive Armor", "Shield", "Silk Touch Book"]], + ["Heart Transplanter", False, [], ["Progressive Armor", "Shield", "Enchanting"]], + ["Heart Transplanter", False, ["Progressive Tools", "Progressive Tools"], ["Progressive Tools", + "Progressive Armor", "Shield"]], + ["Heart Transplanter", True, ["Progressive Resource Crafting", "Progressive Resource Crafting", + "Progressive Weapons", "Progressive Armor", + "Progressive Tools"]], + ["Heart Transplanter", True, ["Progressive Resource Crafting", "Progressive Resource Crafting", + "Progressive Weapons", "Shield", + "Progressive Tools"]], + ["Heart Transplanter", True, ["Progressive Resource Crafting", "Progressive Resource Crafting", + "Progressive Weapons", "Enchanting", "Silk Touch Book", + "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ]) + + def test_42136(self): + self.run_location_tests([ + ["Mob Kabob", False, []], + ["Mob Kabob", False, [], ["Progressive Resource Crafting"]], + ["Mob Kabob", True, ["Progressive Resource Crafting"]] + ]) diff --git a/apworld_src/minecraft/test/test_data_load.py b/apworld_src/minecraft/test/test_data_load.py new file mode 100644 index 0000000..6f96739 --- /dev/null +++ b/apworld_src/minecraft/test/test_data_load.py @@ -0,0 +1,60 @@ +import unittest + +from .. import Constants + + +class TestDataLoad(unittest.TestCase): + + def test_item_data(self): + item_info = Constants.item_info + + # All items in sub-tables are in all_items + all_items: set = set(item_info['all_items']) + assert set(item_info['progression_items']) <= all_items + assert set(item_info['useful_items']) <= all_items + assert set(item_info['trap_items']) <= all_items + assert set(item_info['required_pool'].keys()) <= all_items + assert set(item_info['junk_weights'].keys()) <= all_items + + # No overlapping ids (because of bee trap stuff) + all_ids: set = set(Constants.item_name_to_id.values()) + assert len(all_items) == len(all_ids) + + def test_location_data(self): + location_info = Constants.location_info + exclusion_info = Constants.exclusion_info + + # Every location has a region and every region's locations are in all_locations + all_locations: set = set(location_info['all_locations']) + all_locs_2: set = set() + for v in location_info['locations_by_region'].values(): + all_locs_2.update(v) + assert all_locations == all_locs_2 + + # All exclusions are locations + for v in exclusion_info.values(): + assert set(v) <= all_locations + + def test_region_data(self): + region_info = Constants.region_info + + # Every entrance and region in mandatory/default/illegal connections is a real entrance and region + all_regions = set() + all_entrances = set() + for v in region_info['regions']: + assert isinstance(v[0], str) + assert isinstance(v[1], list) + all_regions.add(v[0]) + all_entrances.update(v[1]) + + for v in region_info['mandatory_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for v in region_info['default_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for k, v in region_info['illegal_connections'].items(): + assert k in all_regions + assert set(v) <= all_entrances diff --git a/apworld_src/minecraft/test/test_entrances.py b/apworld_src/minecraft/test/test_entrances.py new file mode 100644 index 0000000..32e0eaf --- /dev/null +++ b/apworld_src/minecraft/test/test_entrances.py @@ -0,0 +1,97 @@ +from ..test.bases import MCTestBase + + +class TestEntrances(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } + + def testPortals(self): + self.run_entrance_tests([ + ['Nether Portal', False, []], + ['Nether Portal', False, [], ['Flint and Steel']], + ['Nether Portal', False, [], ['Progressive Resource Crafting']], + ['Nether Portal', False, [], ['Progressive Tools']], + ['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket']], + ['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']], + + ['End Portal', False, []], + ['End Portal', False, [], ['Brewing']], + ['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ['End Portal', False, [], ['Flint and Steel']], + ['End Portal', False, [], ['Progressive Resource Crafting']], + ['End Portal', False, [], ['Progressive Tools']], + ['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ['End Portal', False, [], ['Progressive Weapons']], + ['End Portal', False, [], ['Progressive Armor', 'Shield']], + ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ]) + + def testStructures(self): + self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent + ['Overworld Structure 1', False, []], + ['Overworld Structure 1', False, [], ['Progressive Weapons']], + ['Overworld Structure 1', False, [], ['Progressive Resource Crafting', 'Campfire']], + ['Overworld Structure 1', True, ['Progressive Weapons', 'Progressive Resource Crafting']], + ['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']], + + ['Overworld Structure 2', False, []], + ['Overworld Structure 2', False, [], ['Progressive Weapons']], + ['Overworld Structure 2', False, [], ['Progressive Resource Crafting', 'Campfire']], + ['Overworld Structure 2', True, ['Progressive Weapons', 'Progressive Resource Crafting']], + ['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']], + + ['Nether Structure 1', False, []], + ['Nether Structure 1', False, [], ['Flint and Steel']], + ['Nether Structure 1', False, [], ['Progressive Resource Crafting']], + ['Nether Structure 1', False, [], ['Progressive Tools']], + ['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ['Nether Structure 1', False, [], ['Progressive Weapons']], + ['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']], + ['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], + + ['Nether Structure 2', False, []], + ['Nether Structure 2', False, [], ['Flint and Steel']], + ['Nether Structure 2', False, [], ['Progressive Resource Crafting']], + ['Nether Structure 2', False, [], ['Progressive Tools']], + ['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ['Nether Structure 2', False, [], ['Progressive Weapons']], + ['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']], + ['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], + + ['The End Structure', False, []], + ['The End Structure', False, [], ['Brewing']], + ['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ['The End Structure', False, [], ['Flint and Steel']], + ['The End Structure', False, [], ['Progressive Resource Crafting']], + ['The End Structure', False, [], ['Progressive Tools']], + ['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ['The End Structure', False, [], ['Progressive Weapons']], + ['The End Structure', False, [], ['Progressive Armor', 'Shield']], + ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', + 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], + + ]) \ No newline at end of file diff --git a/apworld_src/minecraft/test/test_options.py b/apworld_src/minecraft/test/test_options.py new file mode 100644 index 0000000..a406bbf --- /dev/null +++ b/apworld_src/minecraft/test/test_options.py @@ -0,0 +1,64 @@ +from ..test.bases import MCTestBase +from ..Constants import region_info +from .. import Options + +from BaseClasses import ItemClassification + +class AdvancementTestBase(MCTestBase): + options = { + "advancement_goal": Options.AdvancementGoal.range_end + } + # beatability test implicit + +class ShardTestBase(MCTestBase): + options = { + "egg_shards_required": Options.EggShardsRequired.range_end, + "egg_shards_available": Options.EggShardsAvailable.range_end + } + + # check that itempool is not overfilled with shards + def test_itempool(self): + assert len(self.multiworld.get_unfilled_locations()) == len(self.multiworld.itempool) + +class CompassTestBase(MCTestBase): + def test_compasses_in_pool(self): + structures = [x[1] for x in region_info["default_connections"]] + itempool_str = {item.name for item in self.multiworld.itempool} + for struct in structures: + assert f"Structure Compass ({struct})" in itempool_str + +class UnlockableHeartsTestBase(MCTestBase): + options = { + "unlockable_hearts": True + } + + def test_hearts_in_pool(self): + heart_names = {f"Heart {heart}" for heart in range(1, 10)} + hearts = [item for item in self.multiworld.itempool if item.name in heart_names] + assert {item.name for item in hearts} == heart_names + assert all(item.classification == ItemClassification.progression for item in hearts) + + def test_unlockable_hearts_in_slot_data(self): + slot_data = self.multiworld.worlds[self.player].fill_slot_data() + assert slot_data["unlockable_hearts"] is True + +class NoBeeTestBase(MCTestBase): + options = { + "bee_traps": Options.BeeTraps.range_start + } + + # With no bees, there are no traps in the pool + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.trap + + +class AllBeeTestBase(MCTestBase): + options = { + "bee_traps": Options.BeeTraps.range_end + } + + # With max bees, there are no filler items, only bee traps + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.filler diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975..61285a6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/minecraft.apworld b/minecraft.apworld new file mode 100644 index 0000000..40d0705 Binary files /dev/null and b/minecraft.apworld differ diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_1.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_1.json new file mode 100644 index 0000000..546ab0c --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_1.json @@ -0,0 +1,25 @@ +{ + "parent": "aprandomizer:received/root", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart" + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 1" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_2.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_2.json new file mode 100644 index 0000000..0540ead --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_2.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_1", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 2 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 2" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_3.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_3.json new file mode 100644 index 0000000..cad0939 --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_3.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_2", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 3 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 3" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_4.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_4.json new file mode 100644 index 0000000..2525bf4 --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_4.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_3", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 4 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 4" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_5.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_5.json new file mode 100644 index 0000000..ed5fb1f --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_5.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_4", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 5 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 5" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_6.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_6.json new file mode 100644 index 0000000..db9395e --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_6.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_5", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 6 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 6" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_7.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_7.json new file mode 100644 index 0000000..2ca4004 --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_7.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_6", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 7 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 7" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_8.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_8.json new file mode 100644 index 0000000..7ab61f6 --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_8.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_7", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 8 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 8" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/advancement/received/heart_9.json b/src/generated/resources/data/aprandomizer/advancement/received/heart_9.json new file mode 100644 index 0000000..13a3218 --- /dev/null +++ b/src/generated/resources/data/aprandomizer/advancement/received/heart_9.json @@ -0,0 +1,26 @@ +{ + "parent": "aprandomizer:received/heart_8", + "criteria": { + "received": { + "conditions": { + "item": "aprandomizer:heart", + "tier": 9 + }, + "trigger": "aprandomizer:received_item" + } + }, + "display": { + "announce_to_chat": false, + "description": "Increase your maximum health by one heart.", + "icon": { + "count": 1, + "id": "minecraft:red_dye" + }, + "title": "Heart Unlock 9" + }, + "requirements": [ + [ + "received" + ] + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/aprandomizer/aprandomizer/archipelago_item/heart.json b/src/generated/resources/data/aprandomizer/aprandomizer/archipelago_item/heart.json new file mode 100644 index 0000000..157c3fd --- /dev/null +++ b/src/generated/resources/data/aprandomizer/aprandomizer/archipelago_item/heart.json @@ -0,0 +1,5 @@ +{ + "rewards": { + "type": "aprandomizer:heart" + } +} \ No newline at end of file diff --git a/src/main/java/gg/archipelago/aprandomizer/SlotData.java b/src/main/java/gg/archipelago/aprandomizer/SlotData.java index 671b9b8..597cd1d 100644 --- a/src/main/java/gg/archipelago/aprandomizer/SlotData.java +++ b/src/main/java/gg/archipelago/aprandomizer/SlotData.java @@ -32,6 +32,9 @@ public class SlotData { @SerializedName("death_link") public boolean deathlink = false; + @SerializedName("unlockable_hearts") + public boolean unlockableHearts = false; + @SerializedName("starting_items") public String startingItems; diff --git a/src/main/java/gg/archipelago/aprandomizer/ap/storage/APMCData.java b/src/main/java/gg/archipelago/aprandomizer/ap/storage/APMCData.java index aed8f7e..18e0cd6 100644 --- a/src/main/java/gg/archipelago/aprandomizer/ap/storage/APMCData.java +++ b/src/main/java/gg/archipelago/aprandomizer/ap/storage/APMCData.java @@ -28,6 +28,9 @@ public class APMCData { @SerializedName("advancement_goal") public int advancements_required = -1; + @SerializedName("unlockable_hearts") + public boolean unlockable_hearts = false; + @SerializedName("required_bosses") public Bosses required_bosses = Bosses.ENDER_DRAGON; diff --git a/src/main/java/gg/archipelago/aprandomizer/common/Utils/UnlockableHearts.java b/src/main/java/gg/archipelago/aprandomizer/common/Utils/UnlockableHearts.java new file mode 100644 index 0000000..b224130 --- /dev/null +++ b/src/main/java/gg/archipelago/aprandomizer/common/Utils/UnlockableHearts.java @@ -0,0 +1,43 @@ +package gg.archipelago.aprandomizer.common.Utils; + +import gg.archipelago.aprandomizer.APRandomizer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.Attributes; + +public final class UnlockableHearts { + public static final double STARTING_MAX_HEALTH = 2.0D; + public static final double HEART_HEALTH = 2.0D; + public static final double VANILLA_MAX_HEALTH = 20.0D; + + private UnlockableHearts() { + } + + public static boolean enabled() { + return APRandomizer.getApmcData().unlockable_hearts; + } + + public static void initialize(ServerPlayer player) { + if (!enabled()) return; + setMaxHealth(player, STARTING_MAX_HEALTH); + player.setHealth((float) STARTING_MAX_HEALTH); + } + + public static void grantHeart(ServerPlayer player) { + if (!enabled()) return; + AttributeInstance maxHealth = player.getAttribute(Attributes.MAX_HEALTH); + if (maxHealth == null) return; + double newMaxHealth = Math.min(VANILLA_MAX_HEALTH, maxHealth.getBaseValue() + HEART_HEALTH); + setMaxHealth(player, newMaxHealth); + player.setHealth((float) Math.min(newMaxHealth, player.getHealth() + HEART_HEALTH)); + } + + private static void setMaxHealth(ServerPlayer player, double health) { + AttributeInstance maxHealth = player.getAttribute(Attributes.MAX_HEALTH); + if (maxHealth == null) return; + maxHealth.setBaseValue(health); + if (player.getHealth() > health) { + player.setHealth((float) health); + } + } +} diff --git a/src/main/java/gg/archipelago/aprandomizer/common/commands/StartCommand.java b/src/main/java/gg/archipelago/aprandomizer/common/commands/StartCommand.java index fc09e08..aa6bdce 100644 --- a/src/main/java/gg/archipelago/aprandomizer/common/commands/StartCommand.java +++ b/src/main/java/gg/archipelago/aprandomizer/common/commands/StartCommand.java @@ -5,6 +5,7 @@ import io.github.archipelagomw.ClientStatus; import gg.archipelago.aprandomizer.APRandomizer; import gg.archipelago.aprandomizer.SlotData; +import gg.archipelago.aprandomizer.common.Utils.UnlockableHearts; import gg.archipelago.aprandomizer.common.Utils.Utils; import gg.archipelago.aprandomizer.managers.itemmanager.ItemManager; import net.minecraft.commands.CommandSourceStack; @@ -82,7 +83,11 @@ private static int Start(CommandContext commandSourceCommand server.execute(() -> { for (ServerPlayer player : server.getPlayerList().getPlayers()) { player.getFoodData().eat(20, 20); - player.setHealth(20); + if (UnlockableHearts.enabled()) { + UnlockableHearts.initialize(player); + } else { + player.setHealth(20); + } player.getInventory().clearContent(); player.resetStat(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)); player.teleportTo(spawn.getX(), spawn.getY(), spawn.getZ()); diff --git a/src/main/java/gg/archipelago/aprandomizer/common/events/OnJoin.java b/src/main/java/gg/archipelago/aprandomizer/common/events/OnJoin.java index 9de88ca..a0523c1 100644 --- a/src/main/java/gg/archipelago/aprandomizer/common/events/OnJoin.java +++ b/src/main/java/gg/archipelago/aprandomizer/common/events/OnJoin.java @@ -2,6 +2,8 @@ import gg.archipelago.aprandomizer.APRandomizer; import gg.archipelago.aprandomizer.ap.storage.APMCData; +import gg.archipelago.aprandomizer.attachments.APAttachmentTypes; +import gg.archipelago.aprandomizer.common.Utils.UnlockableHearts; import gg.archipelago.aprandomizer.common.Utils.Utils; import gg.archipelago.aprandomizer.managers.GoalManager; import gg.archipelago.aprandomizer.managers.advancementmanager.AdvancementManager; @@ -52,6 +54,9 @@ else if (data.state == APMCData.State.INVALID_SEED) { } advancementManager.syncAllAdvancements(); goalManager.updateInfoBar(); + if (UnlockableHearts.enabled() && player.getData(APAttachmentTypes.AP_PLAYER).getIndex() == 0) { + UnlockableHearts.initialize(player); + } itemManager.catchUpPlayer(player); if (APRandomizer.isJailPlayers()) { diff --git a/src/main/java/gg/archipelago/aprandomizer/data/advancements/ReceivedAdvancementProvider.java b/src/main/java/gg/archipelago/aprandomizer/data/advancements/ReceivedAdvancementProvider.java index dee3532..788a4f1 100644 --- a/src/main/java/gg/archipelago/aprandomizer/data/advancements/ReceivedAdvancementProvider.java +++ b/src/main/java/gg/archipelago/aprandomizer/data/advancements/ReceivedAdvancementProvider.java @@ -346,6 +346,23 @@ public void generate(Provider registries, Consumer writer) { .addCriterion("auto", PlayerTrigger.TriggerInstance.tick()) .save(writer, Identifier.fromNamespaceAndPath(APRandomizer.MODID, "received/progressive_weapons_after")); + AdvancementHolder heartParent = root; + for (int heart = 1; heart <= 9; heart++) { + heartParent = Advancement.Builder.recipeAdvancement() + .display( + Items.RED_DYE, + Component.literal("Heart Unlock " + heart), + Component.literal("Increase your maximum health by one heart."), + null, + AdvancementType.TASK, + true, + false, + false) + .parent(heartParent) + .addCriterion("received", ReceivedItemCriteria.TriggerInstance.receivedItem(APItems.HEART, heart)) + .save(writer, Identifier.fromNamespaceAndPath(APRandomizer.MODID, "received/heart_" + heart)); + } + AdvancementHolder saddle = Advancement.Builder.recipeAdvancement() .display( Items.SADDLE, diff --git a/src/main/java/gg/archipelago/aprandomizer/items/APItems.java b/src/main/java/gg/archipelago/aprandomizer/items/APItems.java index bd21a4a..46a1ef9 100644 --- a/src/main/java/gg/archipelago/aprandomizer/items/APItems.java +++ b/src/main/java/gg/archipelago/aprandomizer/items/APItems.java @@ -93,6 +93,7 @@ public class APItems { public static final ResourceKey TRAP_BEES = id("trap/bees"); public static final ResourceKey DRAGON_EGG_SHARD = id("dragon_egg_shard"); + public static final ResourceKey HEART = id("heart"); private static ResourceKey id(String name) { return ResourceKey.create(APRegistries.ARCHIPELAGO_ITEM, Identifier.fromNamespaceAndPath(APRandomizer.MODID, name)); @@ -409,6 +410,10 @@ public static void bootstrap(BootstrapContext context) { context.register(DRAGON_EGG_SHARD, APItem.ofReward( new DragonEggShardReward())); + + context.register(HEART, + APItem.ofReward( + new HeartReward())); } private static ItemStack enchantment(Holder enchantment, int level) { diff --git a/src/main/java/gg/archipelago/aprandomizer/items/APRewardTypes.java b/src/main/java/gg/archipelago/aprandomizer/items/APRewardTypes.java index 3e63670..680545d 100644 --- a/src/main/java/gg/archipelago/aprandomizer/items/APRewardTypes.java +++ b/src/main/java/gg/archipelago/aprandomizer/items/APRewardTypes.java @@ -19,4 +19,5 @@ public class APRewardTypes { public static final DeferredHolder, MapCodec> COMPASS = REGISTER.register("compass", () -> CompassReward.MAP_CODEC); public static final DeferredHolder, MapCodec> MOB_TRAP = REGISTER.register("mob_trap", () -> MobTrap.MAP_CODEC); public static final DeferredHolder, MapCodec> DRAGON_EGG_SHARD = REGISTER.register("dragon_egg_shard", () -> DragonEggShardReward.MAP_CODEC); + public static final DeferredHolder, MapCodec> HEART = REGISTER.register("heart", () -> HeartReward.MAP_CODEC); } diff --git a/src/main/java/gg/archipelago/aprandomizer/items/HeartReward.java b/src/main/java/gg/archipelago/aprandomizer/items/HeartReward.java new file mode 100644 index 0000000..b95635a --- /dev/null +++ b/src/main/java/gg/archipelago/aprandomizer/items/HeartReward.java @@ -0,0 +1,19 @@ +package gg.archipelago.aprandomizer.items; + +import com.mojang.serialization.MapCodec; +import gg.archipelago.aprandomizer.common.Utils.UnlockableHearts; +import net.minecraft.server.level.ServerPlayer; + +public record HeartReward() implements APReward { + public static final MapCodec MAP_CODEC = MapCodec.unit(new HeartReward()); + + @Override + public MapCodec codec() { + return MAP_CODEC; + } + + @Override + public void give(ServerPlayer player) { + UnlockableHearts.grantHeart(player); + } +} diff --git a/src/main/java/gg/archipelago/aprandomizer/managers/itemmanager/ItemManager.java b/src/main/java/gg/archipelago/aprandomizer/managers/itemmanager/ItemManager.java index a446871..938fe14 100644 --- a/src/main/java/gg/archipelago/aprandomizer/managers/itemmanager/ItemManager.java +++ b/src/main/java/gg/archipelago/aprandomizer/managers/itemmanager/ItemManager.java @@ -100,6 +100,15 @@ public class ItemManager { map.put(50L, APItems.COMPASS_ANCIENT_CITY); map.put(51L, APItems.COMPASS_TRAIL_RUINS); map.put(52L, APItems.COMPASS_TRIAL_CHAMBERS); + map.put(53L, APItems.HEART); + map.put(54L, APItems.HEART); + map.put(55L, APItems.HEART); + map.put(56L, APItems.HEART); + map.put(57L, APItems.HEART); + map.put(58L, APItems.HEART); + map.put(59L, APItems.HEART); + map.put(60L, APItems.HEART); + map.put(61L, APItems.HEART); }); // long index = 51L;