mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 18:13:48 -07:00
Compare commits
24 Commits
plando-cou
...
0.6.2-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2666bacd7 | ||
|
|
4eefd9c3ce | ||
|
|
211456242e | ||
|
|
6f244c4661 | ||
|
|
47bf6d724b | ||
|
|
5c710ad032 | ||
|
|
dda5a05cbb | ||
|
|
e0a63e0290 | ||
|
|
9246659589 | ||
|
|
377cdb84b4 | ||
|
|
0e759f25fd | ||
|
|
b408bb4f6e | ||
|
|
1356479415 | ||
|
|
ec5b4e704f | ||
|
|
aa9e617510 | ||
|
|
ecb739ce96 | ||
|
|
3b72140435 | ||
|
|
27a6770569 | ||
|
|
2ff611167a | ||
|
|
e83e178b63 | ||
|
|
068a757373 | ||
|
|
0ad4527719 | ||
|
|
8c6327d024 | ||
|
|
aecbb2ab02 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -56,7 +56,6 @@ success.txt
|
||||
output/
|
||||
Output Logs/
|
||||
/factorio/
|
||||
/Minecraft Forge Server/
|
||||
/WebHostLib/static/generated
|
||||
/freeze_requirements.txt
|
||||
/Archipelago.zip
|
||||
@@ -184,12 +183,6 @@ _speedups.c
|
||||
_speedups.cpp
|
||||
_speedups.html
|
||||
|
||||
# minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft_versions.json
|
||||
!worlds/minecraft/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from settings import get_settings
|
||||
from NetUtils import ClientStatus
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
@@ -80,8 +81,8 @@ class AdventureContext(CommonContext):
|
||||
self.local_item_locations = {}
|
||||
self.dragon_speed_info = {}
|
||||
|
||||
options = Utils.get_settings()
|
||||
self.display_msgs = options["adventure_options"]["display_msgs"]
|
||||
options = get_settings().adventure_options
|
||||
self.display_msgs = options.display_msgs
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -102,7 +103,7 @@ class AdventureContext(CommonContext):
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.locations_array = None
|
||||
if Utils.get_settings()["adventure_options"].get("death_link", False):
|
||||
if get_settings().adventure_options.as_dict().get("death_link", False):
|
||||
self.set_deathlink = True
|
||||
async_start(self.get_freeincarnates_used())
|
||||
elif cmd == "RoomInfo":
|
||||
@@ -415,8 +416,9 @@ async def atari_sync_task(ctx: AdventureContext):
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True)
|
||||
rom_args = Utils.get_settings()["adventure_options"].get("rom_args")
|
||||
options = get_settings().adventure_options
|
||||
auto_start = options.rom_start
|
||||
rom_args = options.rom_args
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
@@ -1337,8 +1337,8 @@ class Region:
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
|
||||
created entrances will be named "self.name -> connecting_region"
|
||||
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
|
||||
created entrances will be named "self.name -> connecting_region"
|
||||
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
|
||||
"""
|
||||
if not isinstance(exits, Dict):
|
||||
exits = dict.fromkeys(exits)
|
||||
|
||||
11
Fill.py
11
Fill.py
@@ -890,7 +890,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
worlds = set()
|
||||
for listed_world in target_world:
|
||||
if listed_world not in world_name_lookup:
|
||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
failed(f"Cannot place item to {listed_world}'s world as that world does not exist.",
|
||||
block.force)
|
||||
continue
|
||||
worlds.add(world_name_lookup[listed_world])
|
||||
@@ -923,9 +923,9 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
|
||||
locations_from_groups: list[str] = []
|
||||
resolved_locations: list[Location] = []
|
||||
for target_player in worlds:
|
||||
locations_from_groups: list[str] = []
|
||||
world_locations = multiworld.get_unfilled_locations(target_player)
|
||||
for group in multiworld.worlds[target_player].location_name_groups:
|
||||
if group in locations:
|
||||
@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
|
||||
count = block.count
|
||||
if not count:
|
||||
count = len(new_block.items)
|
||||
count = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||
if new_block.resolved_locations else len(new_block.items))
|
||||
if isinstance(count, int):
|
||||
count = {"min": count, "max": count}
|
||||
if "min" not in count:
|
||||
count["min"] = 0
|
||||
if "max" not in count:
|
||||
count["max"] = len(new_block.items)
|
||||
count["max"] = (min(len(new_block.items), len(new_block.resolved_locations))
|
||||
if new_block.resolved_locations else len(new_block.items))
|
||||
|
||||
|
||||
new_block.count = count
|
||||
plando_blocks[player].append(new_block)
|
||||
|
||||
@@ -290,12 +290,9 @@ async def gba_sync_task(ctx: MMBN3Context):
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
options = Utils.get_options().get("mmbn3_options", None)
|
||||
if options is None:
|
||||
auto_start = True
|
||||
else:
|
||||
auto_start = options.get("rom_start", True)
|
||||
if auto_start:
|
||||
from worlds.mmbn3 import MMBN3World
|
||||
auto_start = MMBN3World.settings.rom_start
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif os.path.isfile(auto_start):
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import atexit
|
||||
import shutil
|
||||
from subprocess import Popen
|
||||
from shutil import copyfile
|
||||
from time import strftime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
import Utils
|
||||
from Utils import is_windows
|
||||
from settings import get_settings
|
||||
|
||||
atexit.register(input, "Press enter to exit.")
|
||||
|
||||
# 1 or more digits followed by m or g, then optional b
|
||||
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
||||
|
||||
|
||||
def prompt_yes_no(prompt):
|
||||
yes_inputs = {'yes', 'ye', 'y'}
|
||||
no_inputs = {'no', 'n'}
|
||||
while True:
|
||||
choice = input(prompt + " [y/n] ").lower()
|
||||
if choice in yes_inputs:
|
||||
return True
|
||||
elif choice in no_inputs:
|
||||
return False
|
||||
else:
|
||||
print('Please respond with "y" or "n".')
|
||||
|
||||
|
||||
def find_ap_randomizer_jar(forge_dir):
|
||||
"""Create mods folder if needed; find AP randomizer jar; return None if not found."""
|
||||
mods_dir = os.path.join(forge_dir, 'mods')
|
||||
if os.path.isdir(mods_dir):
|
||||
for entry in os.scandir(mods_dir):
|
||||
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
|
||||
logging.info(f"Found AP randomizer mod: {entry.name}")
|
||||
return entry.name
|
||||
return None
|
||||
else:
|
||||
os.mkdir(mods_dir)
|
||||
logging.info(f"Created mods folder in {forge_dir}")
|
||||
return None
|
||||
|
||||
|
||||
def replace_apmc_files(forge_dir, apmc_file):
|
||||
"""Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory."""
|
||||
if apmc_file is None:
|
||||
return
|
||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
||||
copy_apmc = True
|
||||
if not os.path.isdir(apdata_dir):
|
||||
os.mkdir(apdata_dir)
|
||||
logging.info(f"Created APData folder in {forge_dir}")
|
||||
for entry in os.scandir(apdata_dir):
|
||||
if entry.name.endswith(".apmc") and entry.is_file():
|
||||
if not os.path.samefile(apmc_file, entry.path):
|
||||
os.remove(entry.path)
|
||||
logging.info(f"Removed {entry.name} in {apdata_dir}")
|
||||
else: # apmc already in apdata
|
||||
copy_apmc = False
|
||||
if copy_apmc:
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
|
||||
|
||||
def read_apmc_file(apmc_file):
|
||||
from base64 import b64decode
|
||||
|
||||
with open(apmc_file, 'r') as f:
|
||||
return json.loads(b64decode(f.read()))
|
||||
|
||||
|
||||
def update_mod(forge_dir, url: str):
|
||||
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||
os.path.basename(url)
|
||||
if ap_randomizer is not None:
|
||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||
|
||||
if ap_randomizer != os.path.basename(url):
|
||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||
f"{os.path.basename(url)}")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
|
||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(url)
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_eula(forge_dir):
|
||||
"""Check if the EULA is agreed to, and prompt the user to read and agree if necessary."""
|
||||
eula_path = os.path.join(forge_dir, "eula.txt")
|
||||
if not os.path.isfile(eula_path):
|
||||
# Create eula.txt
|
||||
with open(eula_path, 'w') as f:
|
||||
f.write("#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\n")
|
||||
f.write(f"#{strftime('%a %b %d %X %Z %Y')}\n")
|
||||
f.write("eula=false\n")
|
||||
with open(eula_path, 'r+') as f:
|
||||
text = f.read()
|
||||
if 'false' in text:
|
||||
# Prompt user to agree to the EULA
|
||||
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
|
||||
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
|
||||
if prompt_yes_no("Do you agree to the EULA?"):
|
||||
f.seek(0)
|
||||
f.write(text.replace('false', 'true'))
|
||||
f.truncate()
|
||||
logging.info(f"Set {eula_path} to true")
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def find_jdk_dir(version: str) -> str:
|
||||
"""get the specified versions jdk directory"""
|
||||
for entry in os.listdir():
|
||||
if os.path.isdir(entry) and entry.startswith(f"jdk{version}"):
|
||||
return os.path.abspath(entry)
|
||||
|
||||
|
||||
def find_jdk(version: str) -> str:
|
||||
"""get the java exe location"""
|
||||
|
||||
if is_windows:
|
||||
jdk = find_jdk_dir(version)
|
||||
jdk_exe = os.path.join(jdk, "bin", "java.exe")
|
||||
if os.path.isfile(jdk_exe):
|
||||
return jdk_exe
|
||||
else:
|
||||
jdk_exe = shutil.which(options.java)
|
||||
if not jdk_exe:
|
||||
jdk_exe = shutil.which("java") # try to fall back to system java
|
||||
if not jdk_exe:
|
||||
raise Exception("Could not find Java. Is Java installed on the system?")
|
||||
return jdk_exe
|
||||
|
||||
|
||||
def download_java(java: str):
|
||||
"""Download Corretto (Amazon JDK)"""
|
||||
|
||||
jdk = find_jdk_dir(java)
|
||||
if jdk is not None:
|
||||
print(f"Removing old JDK...")
|
||||
from shutil import rmtree
|
||||
rmtree(jdk)
|
||||
|
||||
print(f"Downloading Java...")
|
||||
jdk_url = f"https://corretto.aws/downloads/latest/amazon-corretto-{java}-x64-windows-jdk.zip"
|
||||
resp = requests.get(jdk_url)
|
||||
if resp.status_code == 200: # OK
|
||||
print(f"Extracting...")
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
|
||||
zf.extractall()
|
||||
else:
|
||||
print(f"Error downloading Java (status code {resp.status_code}).")
|
||||
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
if not prompt_yes_no("Continue anyways?"):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def install_forge(directory: str, forge_version: str, java_version: str):
|
||||
"""download and install forge"""
|
||||
|
||||
java_exe = find_jdk(java_version)
|
||||
if java_exe is not None:
|
||||
print(f"Downloading Forge {forge_version}...")
|
||||
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
|
||||
resp = requests.get(forge_url)
|
||||
if resp.status_code == 200: # OK
|
||||
forge_install_jar = os.path.join(directory, "forge_install.jar")
|
||||
if not os.path.exists(directory):
|
||||
os.mkdir(directory)
|
||||
with open(forge_install_jar, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
print(f"Installing Forge...")
|
||||
install_process = Popen([java_exe, "-jar", forge_install_jar, "--installServer", directory])
|
||||
install_process.wait()
|
||||
os.remove(forge_install_jar)
|
||||
|
||||
|
||||
def run_forge_server(forge_dir: str, java_version: str, heap_arg: str) -> Popen:
|
||||
"""Run the Forge server."""
|
||||
|
||||
java_exe = find_jdk(java_version)
|
||||
if not os.path.isfile(java_exe):
|
||||
java_exe = "java" # try to fall back on java in the PATH
|
||||
|
||||
heap_arg = max_heap_re.match(heap_arg).group()
|
||||
if heap_arg[-1] in ['b', 'B']:
|
||||
heap_arg = heap_arg[:-1]
|
||||
heap_arg = "-Xmx" + heap_arg
|
||||
|
||||
os_args = "win_args.txt" if is_windows else "unix_args.txt"
|
||||
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, os_args)
|
||||
forge_args = []
|
||||
with open(args_file) as argfile:
|
||||
for line in argfile:
|
||||
forge_args.extend(line.strip().split(" "))
|
||||
|
||||
args = [java_exe, heap_arg, *forge_args, "-nogui"]
|
||||
logging.info(f"Running Forge server: {args}")
|
||||
os.chdir(forge_dir)
|
||||
return Popen(args)
|
||||
|
||||
|
||||
def get_minecraft_versions(version, release_channel="release"):
|
||||
version_file_endpoint = "https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json"
|
||||
resp = requests.get(version_file_endpoint)
|
||||
local = False
|
||||
if resp.status_code == 200: # OK
|
||||
try:
|
||||
data = resp.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
|
||||
local = True
|
||||
else:
|
||||
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
|
||||
local = True
|
||||
|
||||
if local:
|
||||
with open(Utils.user_path("minecraft_versions.json"), 'r') as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
with open(Utils.user_path("minecraft_versions.json"), 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
try:
|
||||
if version:
|
||||
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
|
||||
else:
|
||||
return resp.json()[release_channel][0]
|
||||
except (StopIteration, KeyError):
|
||||
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
|
||||
if release_channel != "release":
|
||||
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
|
||||
else:
|
||||
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def is_correct_forge(forge_dir) -> bool:
|
||||
if os.path.isdir(os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Utils.init_logging("MinecraftClient")
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
||||
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
|
||||
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
|
||||
parser.add_argument('--release_channel', '-r', dest="channel", type=str, action='store',
|
||||
help="Specify release channel to use.")
|
||||
parser.add_argument('--java', '-j', metavar='17', dest='java', type=str, default=False, action='store',
|
||||
help="specify java version.")
|
||||
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
|
||||
help="specify forge version. (Minecraft Version-Forge Version)")
|
||||
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
|
||||
help="specify Mod data version to download.")
|
||||
|
||||
args = parser.parse_args()
|
||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
options = get_settings().minecraft_options
|
||||
channel = args.channel or options.release_channel
|
||||
apmc_data = None
|
||||
data_version = args.data_version or None
|
||||
|
||||
if apmc_file is None and not args.install:
|
||||
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
|
||||
|
||||
if apmc_file is not None and data_version is None:
|
||||
apmc_data = read_apmc_file(apmc_file)
|
||||
data_version = apmc_data.get('client_version', '')
|
||||
|
||||
versions = get_minecraft_versions(data_version, channel)
|
||||
|
||||
forge_dir = options.forge_directory
|
||||
max_heap = options.max_heap_size
|
||||
forge_version = args.forge or versions["forge"]
|
||||
java_version = args.java or versions["java"]
|
||||
mod_url = versions["url"]
|
||||
java_dir = find_jdk_dir(java_version)
|
||||
|
||||
if args.install:
|
||||
if is_windows:
|
||||
print("Installing Java")
|
||||
download_java(java_version)
|
||||
if not is_correct_forge(forge_dir):
|
||||
print("Installing Minecraft Forge")
|
||||
install_forge(forge_dir, forge_version, java_version)
|
||||
else:
|
||||
print("Correct Forge version already found, skipping install.")
|
||||
sys.exit(0)
|
||||
|
||||
if apmc_data is None:
|
||||
raise FileNotFoundError(f"APMC file does not exist or is inaccessible at the given location ({apmc_file})")
|
||||
|
||||
if is_windows:
|
||||
if java_dir is None or not os.path.isdir(java_dir):
|
||||
if prompt_yes_no("Did not find java directory. Download and install java now?"):
|
||||
download_java(java_version)
|
||||
java_dir = find_jdk_dir(java_version)
|
||||
if java_dir is None or not os.path.isdir(java_dir):
|
||||
raise NotADirectoryError(f"Path {java_dir} does not exist or could not be accessed.")
|
||||
|
||||
if not is_correct_forge(forge_dir):
|
||||
if prompt_yes_no(f"Did not find forge version {forge_version} download and install it now?"):
|
||||
install_forge(forge_dir, forge_version, java_version)
|
||||
if not os.path.isdir(forge_dir):
|
||||
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
|
||||
|
||||
if not max_heap_re.match(max_heap):
|
||||
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
||||
|
||||
update_mod(forge_dir, mod_url)
|
||||
replace_apmc_files(forge_dir, apmc_file)
|
||||
check_eula(forge_dir)
|
||||
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
||||
server_process.wait()
|
||||
@@ -7,7 +7,6 @@ Currently, the following games are supported:
|
||||
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
|
||||
6
Utils.py
6
Utils.py
@@ -166,6 +166,10 @@ def home_path(*path: str) -> str:
|
||||
os.symlink(home_path.cached_path, legacy_home_path)
|
||||
else:
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
elif sys.platform == 'darwin':
|
||||
import platformdirs
|
||||
home_path.cached_path = platformdirs.user_data_dir("Archipelago", False)
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
else:
|
||||
# not implemented
|
||||
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
||||
@@ -177,7 +181,7 @@ def user_path(*path: str) -> str:
|
||||
"""Returns either local_path or home_path based on write permissions."""
|
||||
if hasattr(user_path, "cached_path"):
|
||||
pass
|
||||
elif os.access(local_path(), os.W_OK):
|
||||
elif os.access(local_path(), os.W_OK) and not (is_macos and is_frozen()):
|
||||
user_path.cached_path = local_path()
|
||||
else:
|
||||
user_path.cached_path = home_path()
|
||||
|
||||
@@ -61,12 +61,7 @@ def download_slot_file(room_id, player_id: int):
|
||||
else:
|
||||
import io
|
||||
|
||||
if slot_data.game == "Minecraft":
|
||||
from worlds.minecraft import mc_update_output
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
|
||||
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
||||
elif slot_data.game == "Factorio":
|
||||
if slot_data.game == "Factorio":
|
||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("info.json"):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters in the location-table
|
||||
let counters = document.getElementsByClassName('counter');
|
||||
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 384px;
|
||||
background-color: #42b149;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-weight: bold;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 384px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #42b149;
|
||||
padding: 0 3px 3px;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-size: 14px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -26,10 +26,7 @@
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMC File...</a>
|
||||
{% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||
{% if patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APV6 File...</a>
|
||||
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
|
||||
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
|
||||
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
|
||||
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
title="Progressive Resource Crafting" /></td>
|
||||
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Ender Pearl'] }}" class="{{ 'acquired' if '3 Ender Pearls' in acquired_items }}" title="Ender Pearls" />
|
||||
<div class="item-count">{{ pearls_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Bucket'] }}" class="{{ 'acquired' if 'Bucket' in acquired_items }}" title="Bucket" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Archery' in acquired_items }}" title="Archery" /></td>
|
||||
<td><img src="{{ icons['Shield'] }}" class="{{ 'acquired' if 'Shield' in acquired_items }}" title="Shield" /></td>
|
||||
<td><img src="{{ icons['Red Bed'] }}" class="{{ 'acquired' if 'Bed' in acquired_items }}" title="Bed" /></td>
|
||||
<td><img src="{{ icons['Water Bottle'] }}" class="{{ 'acquired' if 'Bottles' in acquired_items }}" title="Bottles" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Netherite Scrap'] }}" class="{{ 'acquired' if '8 Netherite Scrap' in acquired_items }}" title="Netherite Scrap" />
|
||||
<div class="item-count">{{ scrap_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Flint and Steel'] }}" class="{{ 'acquired' if 'Flint and Steel' in acquired_items }}" title="Flint and Steel" /></td>
|
||||
<td><img src="{{ icons['Enchanting Table'] }}" class="{{ 'acquired' if 'Enchanting' in acquired_items }}" title="Enchanting" /></td>
|
||||
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
|
||||
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
|
||||
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Dragon Egg Shard'] }}" class="{{ 'acquired' if 'Dragon Egg Shard' in acquired_items }}" title="Dragon Egg Shard" />
|
||||
<div class="item-count">{{ shard_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Lead'] }}" class="{{ 'acquired' if 'Lead' in acquired_items }}" title="Lead" /></td>
|
||||
<td><img src="{{ icons['Saddle'] }}" class="{{ 'acquired' if 'Saddle' in acquired_items }}" title="Saddle" /></td>
|
||||
<td><img src="{{ icons['Channeling Book'] }}" class="{{ 'acquired' if 'Channeling Book' in acquired_items }}" title="Channeling Book" /></td>
|
||||
<td><img src="{{ icons['Silk Touch Book'] }}" class="{{ 'acquired' if 'Silk Touch Book' in acquired_items }}" title="Silk Touch Book" /></td>
|
||||
<td><img src="{{ icons['Piercing IV Book'] }}" class="{{ 'acquired' if 'Piercing IV Book' in acquired_items }}" title="Piercing IV Book" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -706,127 +706,6 @@ if "A Link to the Past" in network_data_package["games"]:
|
||||
_multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker
|
||||
_player_trackers["A Link to the Past"] = render_ALinkToThePast_tracker
|
||||
|
||||
if "Minecraft" in network_data_package["games"]:
|
||||
def render_Minecraft_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
|
||||
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
||||
"Saddle": "https://i.imgur.com/2QtDyR0.png",
|
||||
"Channeling Book": "https://i.imgur.com/J3WsYZw.png",
|
||||
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
|
||||
"Piercing IV Book": "https://i.imgur.com/OzJptGz.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105,
|
||||
42099, 42103, 42110, 42100],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111,
|
||||
42112,
|
||||
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
|
||||
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
|
||||
inventory = tracker_data.get_player_inventory_counts(team, player)
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_")
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015,
|
||||
"Dragon Egg Shard": 45043
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name + "_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = tracker_data.get_player_client_status(team, player)
|
||||
display_data["game_finished"] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = tracker_data.get_player_checked_locations(team, player)
|
||||
lookup_name = lambda id: tracker_data.location_id_to_name["Minecraft"][id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done["Total"] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area["Total"] = sum(checks_in_area.values())
|
||||
|
||||
lookup_any_item_id_to_name = tracker_data.item_id_to_name["Minecraft"]
|
||||
return render_template(
|
||||
"tracker__Minecraft.html",
|
||||
inventory=inventory,
|
||||
icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
|
||||
player=player,
|
||||
team=team,
|
||||
room=tracker_data.room,
|
||||
player_name=tracker_data.get_player_name(team, player),
|
||||
saving_second=tracker_data.get_room_saving_second(),
|
||||
checks_done=checks_done,
|
||||
checks_in_area=checks_in_area,
|
||||
location_info=location_info,
|
||||
**display_data,
|
||||
)
|
||||
|
||||
_player_trackers["Minecraft"] = render_Minecraft_tracker
|
||||
|
||||
if "Ocarina of Time" in network_data_package["games"]:
|
||||
def render_OcarinaOfTime_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
icons = {
|
||||
|
||||
@@ -135,11 +135,6 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
multidata = None
|
||||
|
||||
# Minecraft
|
||||
elif file.filename.endswith(".apmc"):
|
||||
data = zfile.open(file, "r").read()
|
||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||
files[metadata["player_id"]] = data
|
||||
|
||||
# Factorio
|
||||
elif file.filename.endswith(".zip"):
|
||||
|
||||
@@ -24,9 +24,20 @@
|
||||
<BaseButton>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<MDTabsItemBase>:
|
||||
ripple_color: app.theme_cls.primaryColor
|
||||
ripple_duration_in_fast: 0.2
|
||||
<MDNavigationItemBase>:
|
||||
on_release: app.screens.switch_screens(self)
|
||||
|
||||
MDNavigationItemLabel:
|
||||
text: root.text
|
||||
theme_text_color: "Custom"
|
||||
text_color_active: self.theme_cls.primaryColor
|
||||
text_color_normal: 1, 1, 1, 1
|
||||
# indicator is on icon only for some reason
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: self.theme_cls.secondaryContainerColor if root.active else self.theme_cls.transparentColor
|
||||
Rectangle:
|
||||
size: root.size
|
||||
<TooltipLabel>:
|
||||
adaptive_height: True
|
||||
theme_font_size: "Custom"
|
||||
|
||||
@@ -477,7 +477,7 @@ function main()
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
-- If we're uninitialized, attempt to make the connection.
|
||||
if (frame % 120 == 0) then
|
||||
server:settimeout(2)
|
||||
server:settimeout(120)
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
print('Initial Connection Made')
|
||||
|
||||
BIN
data/mcicon.ico
BIN
data/mcicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -121,9 +121,6 @@
|
||||
# The Messenger
|
||||
/worlds/messenger/ @alwaysintreble
|
||||
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
|
||||
@@ -117,12 +117,6 @@ flowchart LR
|
||||
%% Java Based Games
|
||||
subgraph Java
|
||||
JM[Mod with Archipelago.MultiClient.Java]
|
||||
subgraph Minecraft
|
||||
MCS[Minecraft Forge Server]
|
||||
JMC[Any Java Minecraft Clients]
|
||||
MCS <-- TCP --> JMC
|
||||
end
|
||||
JM <-- Forge Mod Loader --> MCS
|
||||
end
|
||||
AS <-- WebSockets --> JM
|
||||
|
||||
|
||||
@@ -258,31 +258,6 @@ another flag like "progression", it means "an especially useful progression item
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
||||
|
||||
### Events
|
||||
|
||||
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
||||
track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
|
||||
being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
|
||||
never made aware of them and these locations can never be checked, nor can the items be received during play.
|
||||
They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
|
||||
is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
|
||||
relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
|
||||
By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
|
||||
They must not exist in the `name_to_id` lookups, as they have no ID.
|
||||
|
||||
The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import ItemClassification
|
||||
from .subclasses import MyGameLocation, MyGameItem
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
victory_loc = MyGameLocation(self.player, "Victory", None)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
```
|
||||
|
||||
### Regions
|
||||
|
||||
Regions are logical containers that typically hold locations that share some common access rules. If location logic is
|
||||
@@ -339,6 +314,63 @@ avoiding the need for indirect conditions at the expense of performance.
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
reject the placement of an item there.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
Events are used to represent in-game actions (that aren't regular Archipelago locations) when either:
|
||||
|
||||
* We want to show in the spoiler log when the player is expected to perform the in-game action.
|
||||
* It's the cleanest way to represent how that in-game action impacts logic.
|
||||
|
||||
Typical examples include completing the goal, defeating a boss, or flipping a switch that affects multiple areas.
|
||||
|
||||
To be precise: the term "event" on its own refers to the special combination of an "event item" placed on an "event
|
||||
location". Event items and locations are created the same way as normal items and locations, except that they have an
|
||||
`id` of `None`, and an event item must be placed on an event location
|
||||
(and vice versa). Finally, although events are often described as "fake" items and locations, it's important to
|
||||
understand that they are perfectly real during generation.
|
||||
|
||||
The most common way to create an event is to create the event item and the event location, then immediately call
|
||||
`Location.place_locked_item()`:
|
||||
|
||||
```python
|
||||
victory_loc = MyGameLocation(self.player, "Defeat the Final Boss", None, final_boss_arena_region)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
set_rule(victory_loc, lambda state: state.has("Boss Defeating Sword", self.player))
|
||||
```
|
||||
|
||||
Requiring an event to finish the game will make the spoiler log display an additional
|
||||
`Defeat the Final Boss: Victory` line when the player is expected to finish, rather than only showing their last
|
||||
relevant item. But events aren't just about the spoiler log; a more substantial example of using events to structure
|
||||
your logic might be:
|
||||
|
||||
```python
|
||||
water_loc = MyGameLocation(self.player, "Water Level Switch", None, pump_station_region)
|
||||
water_loc.place_locked_item(MyGameItem("Lowered Water Level", ItemClassification.progression, None, self.player))
|
||||
pump_station_region.locations.append(water_loc)
|
||||
set_rule(water_loc, lambda state: state.has("Double Jump", self.player)) # the switch is really high up
|
||||
...
|
||||
basement_loc = MyGameLocation(self.player, "Flooded House - Basement Chest", None, flooded_house_region)
|
||||
flooded_house_region.locations += [upstairs_loc, ground_floor_loc, basement_loc]
|
||||
...
|
||||
set_rule(basement_loc, lambda state: state.has("Lowered Water Level", self.player))
|
||||
```
|
||||
|
||||
This creates a "Lowered Water Level" event and a regular location whose access rule depends on that
|
||||
event being reachable. If you made several more locations the same way, this would ensure all of those locations can
|
||||
only become reachable when the event location is reachable (i.e. when the water level can be lowered), without
|
||||
copy-pasting the event location's access rule and then repeatedly re-evaluating it. Also, the spoiler log will show
|
||||
`Water Level Switch: Lowered Water Level` when the player is expected to do this.
|
||||
|
||||
To be clear, this example could also be modeled with a second Region (perhaps "Un-Flooded House"). Or you could modify
|
||||
the game so flipping that switch checks a regular AP location in addition to lowering the water level.
|
||||
Events are never required, but it may be cleaner to use an event if e.g. flipping that switch affects the logic in
|
||||
dozens of half-flooded areas that would all otherwise need additional Regions, and you don't want it to be a regular
|
||||
location. It depends on the game.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Your World
|
||||
@@ -488,8 +520,8 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||
* `create_items(self)`
|
||||
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
||||
after this step. Locations cannot be moved to different regions after this step.
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions after
|
||||
this step. Locations cannot be moved to different regions after this step. This includes event items and locations.
|
||||
* `set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
* `connect_entrances(self)`
|
||||
|
||||
@@ -138,11 +138,6 @@ Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
176
kvui.py
176
kvui.py
@@ -60,7 +60,10 @@ from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupporting
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
|
||||
from kivymd.uix.screen import MDScreen
|
||||
from kivymd.uix.screenmanager import MDScreenManager
|
||||
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
@@ -726,6 +729,10 @@ class MessageBox(Popup):
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class MDNavigationItemBase(MDNavigationItem):
|
||||
text = StringProperty(None)
|
||||
|
||||
|
||||
class ButtonsPrompt(MDDialog):
|
||||
def __init__(self, title: str, text: str, response: typing.Callable[[str], None],
|
||||
*prompts: str, **kwargs) -> None:
|
||||
@@ -766,58 +773,34 @@ class ButtonsPrompt(MDDialog):
|
||||
)
|
||||
|
||||
|
||||
class ClientTabs(MDTabsSecondary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
class MDScreenManagerBase(MDScreenManager):
|
||||
current_tab: MDNavigationItemBase
|
||||
local_screen_names: list[str]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.local_screen_names = []
|
||||
|
||||
def _check_panel_height(self, *args):
|
||||
self.ids.tab_scroll.height = dp(38)
|
||||
|
||||
def update_indicator(
|
||||
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
||||
) -> None:
|
||||
def update_indicator(*args):
|
||||
indicator_pos = (0, 0)
|
||||
indicator_size = (0, 0)
|
||||
|
||||
item_text_object = self._get_tab_item_text_icon_object()
|
||||
|
||||
if item_text_object:
|
||||
indicator_pos = (
|
||||
instance.x + dp(12),
|
||||
self.indicator.pos[1]
|
||||
if not self._tabs_carousel
|
||||
else self._tabs_carousel.height,
|
||||
)
|
||||
indicator_size = (
|
||||
instance.width - dp(24),
|
||||
self.indicator_height,
|
||||
)
|
||||
|
||||
Animation(
|
||||
pos=indicator_pos,
|
||||
size=indicator_size,
|
||||
d=0 if not self.indicator_anim else self.indicator_duration,
|
||||
t=self.indicator_transition,
|
||||
).start(self.indicator)
|
||||
|
||||
if not instance:
|
||||
self.indicator.pos = (x, self.indicator.pos[1])
|
||||
self.indicator.size = (w, self.indicator_height)
|
||||
def add_widget(self, widget: Widget, *args, **kwargs) -> None:
|
||||
super().add_widget(widget, *args, **kwargs)
|
||||
if "index" in kwargs:
|
||||
self.local_screen_names.insert(kwargs["index"], widget.name)
|
||||
else:
|
||||
Clock.schedule_once(update_indicator)
|
||||
self.local_screen_names.append(widget.name)
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
self.ids.container.remove_widget(tab)
|
||||
self.carousel.remove_widget(content)
|
||||
self.on_size(self, self.size)
|
||||
def switch_screens(self, new_tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called whenever the user clicks a tab to switch to a different screen.
|
||||
|
||||
:param new_tab: The new screen to switch to's tab.
|
||||
"""
|
||||
name = new_tab.text
|
||||
if self.local_screen_names.index(name) > self.local_screen_names.index(self.current_screen.name):
|
||||
self.transition.direction = "left"
|
||||
else:
|
||||
self.transition.direction = "right"
|
||||
self.current = name
|
||||
self.current_tab = new_tab
|
||||
|
||||
|
||||
class CommandButton(MDButton, MDTooltip):
|
||||
@@ -845,6 +828,9 @@ class GameManager(ThemedApp):
|
||||
main_area_container: MDGridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
tabs: MDNavigationBar
|
||||
screens: MDScreenManagerBase
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
self.title = self.base_title
|
||||
self.ctx = ctx
|
||||
@@ -874,7 +860,7 @@ class GameManager(ThemedApp):
|
||||
@property
|
||||
def tab_count(self):
|
||||
if hasattr(self, "tabs"):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return max(1, len(self.tabs.children))
|
||||
return 1
|
||||
|
||||
def on_start(self):
|
||||
@@ -914,30 +900,30 @@ class GameManager(ThemedApp):
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||
self.screens = MDScreenManagerBase(pos_hint={"center_x": 0.5})
|
||||
self.tabs = MDNavigationBar(orientation="horizontal", size_hint_y=None, height=dp(40), set_bars_color=True)
|
||||
# bind the method to the bar for back compatibility
|
||||
self.tabs.remove_tab = self.remove_client_tab
|
||||
self.screens.current_tab = self.add_client_tab(
|
||||
"All" if len(self.logging_pairs) > 1 else "Archipelago",
|
||||
UILog(*(logging.getLogger(logger_name) for logger_name, name in self.logging_pairs)),
|
||||
)
|
||||
self.log_panels["All"] = self.screens.current_tab.content
|
||||
self.screens.current_tab.active = True
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
self.log_panels[display_name] = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||
panel.content = self.log_panels[display_name]
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.carousel.add_widget(panel.content)
|
||||
self.tabs.add_widget(panel)
|
||||
self.add_client_tab(display_name, self.log_panels[display_name])
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = HintLog(self.json_to_kivy_parser)
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout(self.hint_log))
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, cols=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
self.main_area_container.add_widget(self.screens)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
@@ -974,25 +960,61 @@ class GameManager(ThemedApp):
|
||||
|
||||
return self.container
|
||||
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> MDNavigationItemBase:
|
||||
"""
|
||||
Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content.
|
||||
|
||||
:param title: The title of the tab.
|
||||
:param content: The Widget to be added as content for this tab's new MDScreen. Will also be added to the
|
||||
returned tab as tab.content.
|
||||
:param index: The index to insert the tab at. Defaults to -1, meaning the tab will be appended to the end.
|
||||
|
||||
:return: The new tab.
|
||||
"""
|
||||
if self.tabs.children:
|
||||
self.tabs.add_widget(MDDivider(orientation="vertical"))
|
||||
new_tab = MDNavigationItemBase(text=title)
|
||||
new_tab.content = content
|
||||
if -1 < index <= len(self.tabs.carousel.slides):
|
||||
new_tab.bind(on_release=self.tabs.set_active_item)
|
||||
new_tab._tabs = self.tabs
|
||||
self.tabs.ids.container.add_widget(new_tab, index=index)
|
||||
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
||||
new_screen = MDScreen(name=title)
|
||||
new_screen.add_widget(content)
|
||||
if -1 < index <= len(self.tabs.children):
|
||||
remapped_index = len(self.tabs.children) - index
|
||||
self.tabs.add_widget(new_tab, index=remapped_index)
|
||||
self.screens.add_widget(new_screen, index=index)
|
||||
else:
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
self.screens.add_widget(new_screen)
|
||||
return new_tab
|
||||
|
||||
def remove_client_tab(self, tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called to remove a tab and its screen.
|
||||
|
||||
:param tab: The tab to remove.
|
||||
"""
|
||||
tab_index = self.tabs.children.index(tab)
|
||||
# if the tab is currently active we need to swap before removing it
|
||||
if tab == self.screens.current_tab:
|
||||
if not tab_index:
|
||||
# account for the divider
|
||||
swap_index = tab_index + 2
|
||||
else:
|
||||
swap_index = tab_index - 2
|
||||
self.tabs.children[swap_index].on_release()
|
||||
# self.screens.switch_screens(self.tabs.children[swap_index])
|
||||
# get the divider to the left if we can
|
||||
if not tab_index:
|
||||
divider_index = tab_index + 1
|
||||
else:
|
||||
divider_index = tab_index - 1
|
||||
self.tabs.remove_widget(self.tabs.children[divider_index])
|
||||
self.tabs.remove_widget(tab)
|
||||
self.screens.remove_widget(self.screens.get_screen(tab.text))
|
||||
|
||||
def update_texts(self, dt):
|
||||
for slide in self.tabs.carousel.slides:
|
||||
if hasattr(slide, "fix_heights"):
|
||||
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if hasattr(self.screens.current_tab.content, "fix_heights"):
|
||||
getattr(self.screens.current_tab.content, "fix_heights")()
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
f" | Connected to: {self.ctx.server_address} " \
|
||||
|
||||
@@ -221,9 +221,6 @@ components: List[Component] = [
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Minecraft
|
||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
||||
file_identifier=SuffixIdentifier('.apmc')),
|
||||
# Ocarina of Time
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
@@ -246,6 +243,5 @@ components: List[Component] = [
|
||||
# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used
|
||||
icon_paths = {
|
||||
'icon': local_path('data', 'icon.png'),
|
||||
'mcicon': local_path('data', 'mcicon.png'),
|
||||
'discord': local_path('data', 'discord-mark-blue.png'),
|
||||
}
|
||||
|
||||
@@ -548,10 +548,12 @@ def set_up_take_anys(multiworld, world, player):
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
multiworld.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword']
|
||||
if swords:
|
||||
sword = multiworld.random.choice(swords)
|
||||
multiworld.itempool.remove(sword)
|
||||
sword_indices = [
|
||||
index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword'
|
||||
]
|
||||
if sword_indices:
|
||||
sword_index = multiworld.random.choice(sword_indices)
|
||||
sword = multiworld.itempool.pop(sword_index)
|
||||
multiworld.itempool.append(item_factory('Rupees (20)', world))
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0)
|
||||
loc_name = "Old Man Sword Cave"
|
||||
|
||||
@@ -38,7 +38,7 @@ class DungeonFillTestBase(TestCase):
|
||||
def test_original_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_original_dungeon)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with (self.subTest(location=location)):
|
||||
with (self.subTest(location_name=location.name)):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
@@ -52,7 +52,7 @@ class DungeonFillTestBase(TestCase):
|
||||
def test_own_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_own_dungeons)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with self.subTest(location=location):
|
||||
with self.subTest(location_name=location.name):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@ Date: Fri, 15 Mar 2024 18:41:40 +0000
|
||||
Description: Used to manage Regions in the Aquaria game multiworld randomizer
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Iterable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState
|
||||
from .Items import AquariaItem, ItemNames
|
||||
from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames
|
||||
@@ -34,10 +34,15 @@ def _has_li(state: CollectionState, player: int) -> bool:
|
||||
return state.has(ItemNames.LI_AND_LI_SONG, player)
|
||||
|
||||
|
||||
def _has_damaging_item(state: CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has the shield song item"""
|
||||
return state.has_any({ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, ItemNames.LI_AND_LI_SONG,
|
||||
ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, ItemNames.BABY_BLASTER}, player)
|
||||
DAMAGING_ITEMS:Iterable[str] = [
|
||||
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||
ItemNames.BABY_BLASTER
|
||||
]
|
||||
|
||||
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
|
||||
"""`player` in `state` has the an item that do damage other than the ones in `to_remove`"""
|
||||
return state.has_any(damaging_items, player)
|
||||
|
||||
|
||||
def _has_energy_attack_item(state: CollectionState, player: int) -> bool:
|
||||
@@ -566,9 +571,11 @@ class AquariaRegions:
|
||||
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_turtle,
|
||||
lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
||||
self.__connect_one_way_regions(self.openwater_tr_turtle, self.openwater_tr)
|
||||
damaging_items_minus_nature_form = [item for item in DAMAGING_ITEMS if item != ItemNames.NATURE_FORM]
|
||||
self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_urns,
|
||||
lambda state: _has_bind_song(state, self.player) or
|
||||
_has_damaging_item(state, self.player))
|
||||
_has_damaging_item(state, self.player,
|
||||
damaging_items_minus_nature_form))
|
||||
self.__connect_regions(self.openwater_tr, self.openwater_br)
|
||||
self.__connect_regions(self.openwater_tr, self.mithalas_city)
|
||||
self.__connect_regions(self.openwater_tr, self.veil_b)
|
||||
|
||||
@@ -75,6 +75,13 @@ class DarkSouls3World(World):
|
||||
"""The pool of all items within this particular world. This is a subset of
|
||||
`self.multiworld.itempool`."""
|
||||
|
||||
missable_dupe_prog_locs: Set[str] = {"PC: Storm Ruler - Siegward",
|
||||
"US: Pyromancy Flame - Cornyx",
|
||||
"US: Tower Key - kill Irina"}
|
||||
"""Locations whose vanilla item is a missable duplicate of a non-missable progression item.
|
||||
If vanilla, these locations shouldn't be expected progression, so they aren't created and don't get rules.
|
||||
"""
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.all_excluded_locations = set()
|
||||
@@ -258,10 +265,7 @@ class DarkSouls3World(World):
|
||||
new_location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
# Don't allow missable duplicates of progression items to be expected progression.
|
||||
if location.name in {"PC: Storm Ruler - Siegward",
|
||||
"US: Pyromancy Flame - Cornyx",
|
||||
"US: Tower Key - kill Irina"}:
|
||||
continue
|
||||
if location.name in self.missable_dupe_prog_locs: continue
|
||||
|
||||
# Replace non-randomized items with events that give the default item
|
||||
event_item = (
|
||||
@@ -1286,8 +1290,9 @@ class DarkSouls3World(World):
|
||||
data = location_dictionary[location]
|
||||
if data.dlc and not self.options.enable_dlc: continue
|
||||
if data.ngp and not self.options.enable_ngp: continue
|
||||
# Don't add rules to missable duplicates of progression items
|
||||
if location in self.missable_dupe_prog_locs and not self._is_location_available(location): continue
|
||||
|
||||
if not self._is_location_available(location): continue
|
||||
if isinstance(rule, str):
|
||||
assert item_dictionary[rule].classification == ItemClassification.progression
|
||||
rule = lambda state, item=rule: state.has(item, self.player)
|
||||
|
||||
@@ -73,7 +73,7 @@ things to keep in mind:
|
||||
|
||||
* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
|
||||
|
||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/6.0
|
||||
[WINE]: https://www.winehq.org/
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -288,7 +288,7 @@ world and the beginning of another world. You can also combine multiple files by
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
description: Example of generating multiple worlds. World 1 of 3
|
||||
description: Example of generating multiple worlds. World 1 of 2
|
||||
name: Mario
|
||||
game: Super Mario 64
|
||||
requires:
|
||||
@@ -310,31 +310,6 @@ Super Mario 64:
|
||||
|
||||
---
|
||||
|
||||
description: Example of generating multiple worlds. World 2 of 3
|
||||
name: Minecraft
|
||||
game: Minecraft
|
||||
Minecraft:
|
||||
progression_balancing: 50
|
||||
accessibility: items
|
||||
advancement_goal: 40
|
||||
combat_difficulty: hard
|
||||
include_hard_advancements: false
|
||||
include_unreasonable_advancements: false
|
||||
include_postgame_advancements: false
|
||||
shuffle_structures: true
|
||||
structure_compasses: true
|
||||
send_defeated_mobs: true
|
||||
bee_traps: 15
|
||||
egg_shards_required: 7
|
||||
egg_shards_available: 10
|
||||
required_bosses:
|
||||
none: 0
|
||||
ender_dragon: 1
|
||||
wither: 0
|
||||
both: 0
|
||||
|
||||
---
|
||||
|
||||
description: Example of generating multiple worlds. World 2 of 2
|
||||
name: ExampleFinder
|
||||
game: ChecksFinder
|
||||
@@ -344,6 +319,6 @@ ChecksFinder:
|
||||
accessibility: items
|
||||
```
|
||||
|
||||
The above example will generate 3 worlds - one Super Mario 64, one Minecraft, and one ChecksFinder.
|
||||
The above example will generate 2 worlds - one Super Mario 64 and one ChecksFinder.
|
||||
|
||||
|
||||
|
||||
@@ -27,73 +27,176 @@ requires:
|
||||
plando: bosses, items, texts, connections
|
||||
```
|
||||
|
||||
For a basic understanding of YAML files, refer to
|
||||
[YAML Formatting](/tutorial/Archipelago/advanced_settings/en#yaml-formatting)
|
||||
in Advanced Settings.
|
||||
|
||||
## Item Plando
|
||||
Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a
|
||||
list of specific locations both in their own game or in another player's game.
|
||||
|
||||
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and
|
||||
`location`, or `items` and `locations`.
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or
|
||||
false and defaults to true if omitted.
|
||||
* `world` is the target world to place the item in.
|
||||
* It gets ignored if only one world is generated.
|
||||
* Can be a number, name, true, false, null, or a list. False is the default.
|
||||
* If a number is used, it targets that slot or player number in the multiworld.
|
||||
* If a name is used, it will target the world with that player name.
|
||||
* If set to true, it will be any player's world besides your own.
|
||||
* If set to false, it will target your own world.
|
||||
* If set to null, it will target a random world in the multiworld.
|
||||
* If a list of names is used, it will target the games with the player names specified.
|
||||
* `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false,
|
||||
or silent. Silent is the default.
|
||||
* If set to true, the item must be placed and the generator will throw an error if it is unable to do so.
|
||||
* If set to false, the generator will log a warning if the placement can't be done but will still generate.
|
||||
* If set to silent and the placement fails, it will be ignored entirely.
|
||||
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and
|
||||
if omitted will default to 100.
|
||||
* Single Placement is when you use a plando block to place a single item at a single location.
|
||||
* `item` is the item you would like to place and `location` is the location to place it.
|
||||
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
|
||||
* `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool.
|
||||
* `locations` is a list of possible locations those items can be placed in.
|
||||
* Some special location group names can be specified:
|
||||
* `early_locations` will add all sphere 1 locations (locations logically reachable only with your starting inventory)
|
||||
* `non_early_locations` will add all locations beyond sphere 1 (locations that require finding at least one item before they become logically reachable)
|
||||
* Using the multi placement method, placements are picked randomly.
|
||||
Item Plando allows a player to place an item in a specific location or locations, or place multiple items into a list
|
||||
of specific locations in their own game and/or in another player's game.
|
||||
|
||||
* `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items`
|
||||
* If a number is used, it will try to place this number of items.
|
||||
* If set to false, it will try to place as many items from the block as it can.
|
||||
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random.
|
||||
To add item plando to your player yaml, you add them under the `plando_items` block. You should start with `item` if you
|
||||
want to do Single Placement, or `items` if you want to do Multi Placement. A list of items can still be defined under
|
||||
`item` but only one of them will be chosen at random to be used.
|
||||
|
||||
After you define `item/items`, you would define `location` or `locations`, depending on if you want to fill one
|
||||
location or many. Note that both `location` and `locations` are optional. A list of locations can still be defined under
|
||||
`location` but only one of them will be chosen at random to be used.
|
||||
|
||||
You may do any combination of `item/items` and `location/locations` in a plando block, but the block only places items
|
||||
in locations **until the shorter of the two lists is used up.**
|
||||
|
||||
Once you are satisfied with your first block, you may continue to define ones under the same `plando_items` parent.
|
||||
Each block can have several different options to tailor it the way you like.
|
||||
|
||||
* The `items` section defines the items to use. Each item name can be followed by a colon and a value.
|
||||
* A numerical value indicates the amount of that item.
|
||||
* A `true` value uses all copies of that item that are in your item pool.
|
||||
|
||||
* The `item` section defines a list of items to use, from which one will be chosen at random. Each item name can be
|
||||
followed by a colon and a value. The value indicates the weight of that item being chosen.
|
||||
|
||||
* The `locations` section defines possible locations those items can be placed in. Two special location groups exist:
|
||||
* `early_locations` will add all sphere 1 locations (locations logically reachable only with your starting
|
||||
inventory).
|
||||
* `non_early_locations` will add all locations beyond sphere 1 (locations that require finding at least one item
|
||||
before they become logically reachable).
|
||||
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *created* from scratch.
|
||||
* `false`: Create a new item with the same name (the world will determine its properties e.g. classification).
|
||||
* `true`: Take the existing item, if it exists, from the item pool. If it does not exist, one will be created from
|
||||
scratch. **(Default)**
|
||||
|
||||
* `world` is the target world to place the item in. It gets ignored if only one world is generated.
|
||||
* **A number:** Use this slot or player number in the multiworld.
|
||||
* **A name:** Use the world with that player name.
|
||||
* **A list of names:** Use the worlds with the player names specified.
|
||||
* `true`: Locations will be in any player's world besides your own.
|
||||
* `false`: Locations will be in your own world. **(Default)**
|
||||
* `null`: Locations will be in a random world in the multiworld.
|
||||
|
||||
* `force` determines whether the generator will fail if the plando block cannot be fulfilled.
|
||||
* `true`: The generator will throw an error if it is unable to place an item.
|
||||
* `false`: The generator will log a warning if it is unable to place an item, but it will still generate.
|
||||
* `silent`: If the placement fails, it will be ignored entirely. **(Default)**
|
||||
|
||||
* `percentage` is the percentage chance for the block to trigger. This can be any integer from 0 to 100.
|
||||
**(Default: 100)**
|
||||
|
||||
* `count` sets the number of items placed from the list.
|
||||
* **Default: 1 if using `item` or `location`, and `false` otherwise.**
|
||||
* **A number:** It will place this number of items.
|
||||
* `false`: It will place as many items from the list as it can.
|
||||
* **If `min` is defined,** it will place at least `min` many items (can be combined with `max`).
|
||||
* **If `max` is defined,** it will place at most `max` many items (can be combined with `min`).
|
||||
|
||||
### Available Items and Locations
|
||||
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive.
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and
|
||||
locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. Names are
|
||||
case-sensitive. You can also use item groups and location groups that are defined in the datapackage.
|
||||
|
||||
### Examples
|
||||
## Item Plando Examples
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Pokémon Red and Blue
|
||||
- items:
|
||||
Potion: 3
|
||||
locations:
|
||||
- "Route 1 - Free Sample Man"
|
||||
- "Mt Moon 1F - West Item"
|
||||
- "Mt Moon 1F - South Item"
|
||||
```
|
||||
This block will lock 3 Potion items on the Route 1 Pokémart employee and 2 Mt Moon items. Note these are all
|
||||
Potions in the vanilla game. The world value has not been specified, so these locations must be in this player's own
|
||||
world by default.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# example block 1 - Timespinner
|
||||
# Example block - A Link to the Past
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
world:
|
||||
- BobsWitness
|
||||
- BobsRogueLegacy
|
||||
count:
|
||||
min: 1
|
||||
max: 4
|
||||
```
|
||||
This block will attempt to place a random number, between 1 and 4, of Progressive Swords into any locations within the
|
||||
game slots named "BobsWitness" and "BobsRogueLegacy."
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Secret of Evermore
|
||||
- items:
|
||||
Levitate: 1
|
||||
Revealer: 1
|
||||
Energize: 1
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Desert Discard
|
||||
world: true
|
||||
count: 2
|
||||
```
|
||||
This block will choose 2 from the Levitate, Revealer, and Energize items at random and attempt to put them into the
|
||||
locations named "Master Sword Pedestal" and "Desert Discard". Because the world value is `true`, these locations
|
||||
must be in other players' worlds.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Timespinner
|
||||
- item:
|
||||
Empire Orb: 1
|
||||
Radiant Orb: 1
|
||||
Radiant Orb: 3
|
||||
location: Starter Chest 1
|
||||
from_pool: true
|
||||
from_pool: false
|
||||
world: true
|
||||
percentage: 50
|
||||
|
||||
# example block 2 - Ocarina of Time
|
||||
```
|
||||
This block will place a single item, either the Empire Orb or Radiant Orb, on the location "Starter Chest 1". There is
|
||||
a 25% chance it is Empire Orb, and 75% chance it is Radiant Orb (1 to 3 odds). The world value is `true`, so this
|
||||
location must be in another player's world. Because the from_pool value is `false`, a copy of these items is added to
|
||||
these locations, while the originals remain in the item pool to be shuffled. Unlike the previous examples, which will
|
||||
always trigger, this block only has a 50% chance to trigger.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Factorio
|
||||
- items:
|
||||
progressive-electric-energy-distribution: 2
|
||||
electric-energy-accumulators: 1
|
||||
progressive-turret: 2
|
||||
locations:
|
||||
- AP-1-001
|
||||
- AP-1-002
|
||||
- AP-1-003
|
||||
- AP-1-004
|
||||
percentage: 80
|
||||
force: true
|
||||
from_pool: true
|
||||
world: false
|
||||
```
|
||||
This block lists 5 items but only 4 locations, so it will place all but 1 of the items randomly among the locations
|
||||
chosen here. This block has an 80% chance of occurring. Because force is `true`, the Generator will fail if it cannot
|
||||
place one of the selected items (not including the fifth item). From_pool and World have been set to their default
|
||||
values here, but they can be omitted and have the same result: items will be removed from the pool, and the locations
|
||||
are in this player's own world.
|
||||
|
||||
**NOTE:** Factorio's locations are dynamically generated, so the locations listed above may not exist in your game,
|
||||
they are here for demonstration only.
|
||||
|
||||
```yaml
|
||||
plando_items:
|
||||
# Example block - Ocarina of Time
|
||||
- items:
|
||||
Kokiri Sword: 1
|
||||
Biggoron Sword: 1
|
||||
Bow: 1
|
||||
Magic Meter: 1
|
||||
Progressive Strength Upgrade: 3
|
||||
Progressive Hookshot: 2
|
||||
locations:
|
||||
- Deku Tree Slingshot Chest
|
||||
- Dodongos Cavern Bomb Bag Chest
|
||||
- Jabu Jabus Belly Boomerang Chest
|
||||
- Bottom of the Well Lens of Truth Chest
|
||||
@@ -102,53 +205,16 @@ A list of all available items and locations can be found in the [website's datap
|
||||
- Water Temple Longshot Chest
|
||||
- Shadow Temple Hover Boots Chest
|
||||
- Spirit Temple Silver Gauntlets Chest
|
||||
world: false
|
||||
|
||||
# example block 3 - Factorio
|
||||
- items:
|
||||
progressive-electric-energy-distribution: 2
|
||||
electric-energy-accumulators: 1
|
||||
progressive-turret: 2
|
||||
locations:
|
||||
- military
|
||||
- gun-turret
|
||||
- logistic-science-pack
|
||||
- steel-processing
|
||||
percentage: 80
|
||||
force: true
|
||||
|
||||
# example block 4 - Secret of Evermore
|
||||
- items:
|
||||
Levitate: 1
|
||||
Revealer: 1
|
||||
Energize: 1
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Boss Relic 1
|
||||
world: true
|
||||
count: 2
|
||||
|
||||
# example block 5 - A Link to the Past
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
world:
|
||||
- BobsSlaytheSpire
|
||||
- BobsRogueLegacy
|
||||
count:
|
||||
min: 1
|
||||
max: 4
|
||||
from_pool: false
|
||||
|
||||
- item: Kokiri Sword
|
||||
location: Deku Tree Slingshot Chest
|
||||
from_pool: false
|
||||
```
|
||||
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
|
||||
player's Starter Chest 1 and removes the chosen item from the item pool.
|
||||
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
||||
in their own dungeon major item chests.
|
||||
3. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
|
||||
four locations chosen here.
|
||||
4. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
|
||||
other players' Master Sword Pedestals or Boss Relic 1 locations.
|
||||
5. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
|
||||
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
|
||||
|
||||
The first block will place the player's Biggoron Sword, Bow, Magic Meter, strength upgrades, and hookshots in the
|
||||
dungeon major item chests. Because the from_pool value is `false`, a copy of these items is added to these locations,
|
||||
while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku
|
||||
Tree Slingshot Chest, again not from the pool.
|
||||
|
||||
## Boss Plando
|
||||
|
||||
@@ -194,7 +260,7 @@ relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%
|
||||
|
||||
## Connection Plando
|
||||
|
||||
This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their
|
||||
This is currently only supported by a few games, including A Link to the Past and Ocarina of Time. As the way that these games interact with their
|
||||
connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in
|
||||
its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
|
||||
@@ -207,7 +273,6 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
|
||||
[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
|
||||
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/data/regions.json#L18****)
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -223,19 +288,10 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
||||
- entrance: Agahnims Tower
|
||||
exit: Old Man Cave Exit (West)
|
||||
direction: exit
|
||||
|
||||
# example block 2 - Minecraft
|
||||
- entrance: Overworld Structure 1
|
||||
exit: Nether Fortress
|
||||
direction: both
|
||||
- entrance: Overworld Structure 2
|
||||
exit: Village
|
||||
direction: both
|
||||
|
||||
```
|
||||
|
||||
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
|
||||
when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to
|
||||
the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take
|
||||
you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower.
|
||||
2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the
|
||||
Minecraft connection plando to work structure shuffle must be enabled.
|
||||
|
||||
@@ -90,7 +90,7 @@ def cmd_gift(self: "SNIClientCommandProcessor") -> None:
|
||||
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
|
||||
f"{self.ctx.slot}":
|
||||
{
|
||||
"IsOpen": handler.gifting,
|
||||
"is_open": handler.gifting,
|
||||
**kdl3_gifting_options
|
||||
}
|
||||
}))
|
||||
@@ -175,11 +175,11 @@ class KDL3SNIClient(SNIClient):
|
||||
key, gift = ctx.stored_data[self.giftbox_key].popitem()
|
||||
await pop_object(ctx, self.giftbox_key, key)
|
||||
# first, special cases
|
||||
traits = [trait["Trait"] for trait in gift["Traits"]]
|
||||
traits = [trait["trait"] for trait in gift["traits"]]
|
||||
if "Candy" in traits or "Invincible" in traits:
|
||||
# apply invincibility candy
|
||||
self.item_queue.append(0x43)
|
||||
elif "Tomato" in traits or "tomato" in gift["ItemName"].lower():
|
||||
elif "Tomato" in traits or "tomato" in gift["item_name"].lower():
|
||||
# apply maxim tomato
|
||||
# only want tomatos here, no other vegetable is that good
|
||||
self.item_queue.append(0x42)
|
||||
@@ -187,7 +187,7 @@ class KDL3SNIClient(SNIClient):
|
||||
# Apply 1-Up
|
||||
self.item_queue.append(0x41)
|
||||
elif "Currency" in traits or "Star" in traits:
|
||||
value = gift["ItemValue"]
|
||||
value = gift.get("item_value", 1)
|
||||
if value >= 50000:
|
||||
self.item_queue.append(0x46)
|
||||
elif value >= 30000:
|
||||
@@ -210,8 +210,8 @@ class KDL3SNIClient(SNIClient):
|
||||
# check if it's tasty
|
||||
if any(x in traits for x in ["Consumable", "Food", "Drink", "Heal", "Health"]):
|
||||
# it's tasty!, use quality to decide how much to heal
|
||||
quality = max((trait["Quality"] for trait in gift["Traits"]
|
||||
if trait["Trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
|
||||
quality = max((trait.get("quality", 1.0) for trait in gift["traits"]
|
||||
if trait["trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
|
||||
quality = min(10, quality * 2)
|
||||
else:
|
||||
# it's not really edible, but he'll eat it anyway
|
||||
@@ -236,23 +236,23 @@ class KDL3SNIClient(SNIClient):
|
||||
for slot, info in ctx.stored_data[self.motherbox_key].items():
|
||||
if int(slot) == ctx.slot and len(ctx.stored_data[self.motherbox_key]) > 1:
|
||||
continue
|
||||
desire = len(set(info["DesiredTraits"]).intersection([trait["Trait"] for trait in gift_base["Traits"]]))
|
||||
desire = len(set(info["desired_traits"]).intersection([trait["trait"] for trait in gift_base["traits"]]))
|
||||
if desire > most_applicable:
|
||||
most_applicable = desire
|
||||
most_applicable_slot = int(slot)
|
||||
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
|
||||
elif most_applicable_slot == ctx.slot and most_applicable == -1 and info["accepts_any_gift"]:
|
||||
# only send to ourselves if no one else will take it
|
||||
most_applicable_slot = int(slot)
|
||||
# print(most_applicable, most_applicable_slot)
|
||||
item_uuid = uuid.uuid4().hex
|
||||
item = {
|
||||
**gift_base,
|
||||
"ID": item_uuid,
|
||||
"Sender": ctx.player_names[ctx.slot],
|
||||
"Receiver": ctx.player_names[most_applicable_slot],
|
||||
"SenderTeam": ctx.team,
|
||||
"ReceiverTeam": ctx.team, # for the moment
|
||||
"IsRefund": False
|
||||
"id": item_uuid,
|
||||
"sender_slot": ctx.slot,
|
||||
"receiver_slot": most_applicable_slot,
|
||||
"sender_team": ctx.team,
|
||||
"receiver_team": ctx.team, # for the moment
|
||||
"is_refund": False
|
||||
}
|
||||
# print(item)
|
||||
await update_object(ctx, f"Giftbox;{ctx.team};{most_applicable_slot}", {
|
||||
@@ -276,8 +276,9 @@ class KDL3SNIClient(SNIClient):
|
||||
if not self.initialize_gifting:
|
||||
self.giftbox_key = f"Giftbox;{ctx.team};{ctx.slot}"
|
||||
self.motherbox_key = f"Giftboxes;{ctx.team}"
|
||||
enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
|
||||
enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x02)
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key,
|
||||
bool(int.from_bytes(enable_gifting, "little")))
|
||||
self.initialize_gifting = True
|
||||
# can't check debug anymore, without going and copying the value. might be important later.
|
||||
if not self.levels:
|
||||
@@ -350,19 +351,19 @@ class KDL3SNIClient(SNIClient):
|
||||
self.item_queue.append(item_idx | 0x80)
|
||||
|
||||
# handle gifts here
|
||||
gifting_status = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
|
||||
if hasattr(ctx, "gifting") and ctx.gifting:
|
||||
if gifting_status[0]:
|
||||
gifting_status = int.from_bytes(await snes_read(ctx, KDL3_GIFTING_FLAG, 0x02), "little")
|
||||
if hasattr(self, "gifting") and self.gifting:
|
||||
if gifting_status:
|
||||
gift = await snes_read(ctx, KDL3_GIFTING_SEND, 0x01)
|
||||
if gift[0]:
|
||||
# we have a gift to send
|
||||
await self.pick_gift_recipient(ctx, gift[0])
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_SEND, bytes([0x00]))
|
||||
else:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01]))
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01, 0x00]))
|
||||
else:
|
||||
if gifting_status[0]:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00]))
|
||||
if gifting_status:
|
||||
snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00, 0x00]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
@@ -37,157 +37,158 @@ async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_ke
|
||||
ctx.set_notify(motherbox_key, giftbox_key)
|
||||
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
|
||||
{
|
||||
"IsOpen": is_open,
|
||||
"is_open": is_open,
|
||||
**kdl3_gifting_options
|
||||
}})
|
||||
await update_object(ctx, f"Giftbox;{ctx.team};{ctx.slot}", {})
|
||||
ctx.client_handler.gifting = is_open
|
||||
|
||||
|
||||
kdl3_gifting_options = {
|
||||
"AcceptsAnyGift": True,
|
||||
"DesiredTraits": [
|
||||
"accepts_any_gift": True,
|
||||
"desired_traits": [
|
||||
"Consumable", "Food", "Drink", "Candy", "Tomato",
|
||||
"Invincible", "Life", "Heal", "Health", "Trap",
|
||||
"Goo", "Gel", "Slow", "Slowness", "Eject", "Removal"
|
||||
],
|
||||
"MinimumGiftVersion": 2,
|
||||
"minimum_gift_version": 3,
|
||||
}
|
||||
|
||||
kdl3_gifts = {
|
||||
1: {
|
||||
"ItemName": "1-Up",
|
||||
"Amount": 1,
|
||||
"ItemValue": 400000,
|
||||
"Traits": [
|
||||
"item_name": "1-Up",
|
||||
"amount": 1,
|
||||
"item_value": 400000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Life",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Life",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
2: {
|
||||
"ItemName": "Maxim Tomato",
|
||||
"Amount": 1,
|
||||
"ItemValue": 500000,
|
||||
"Traits": [
|
||||
"item_name": "Maxim Tomato",
|
||||
"amount": 1,
|
||||
"item_value": 500000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Heal",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Heal",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Food",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Food",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Tomato",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Tomato",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Vegetable",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Vegetable",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
"ItemName": "Energy Drink",
|
||||
"Amount": 1,
|
||||
"ItemValue": 100000,
|
||||
"Traits": [
|
||||
"item_name": "Energy Drink",
|
||||
"amount": 1,
|
||||
"item_value": 100000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Consumable",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Consumable",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Heal",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Heal",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Drink",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Drink",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
]
|
||||
},
|
||||
5: {
|
||||
"ItemName": "Small Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Small Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
6: {
|
||||
"ItemName": "Medium Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 30000,
|
||||
"Traits": [
|
||||
"item_name": "Medium Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 30000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 3,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 3,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 3,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 3,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 3,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 3,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
7: {
|
||||
"ItemName": "Large Star Piece",
|
||||
"Amount": 1,
|
||||
"ItemValue": 50000,
|
||||
"Traits": [
|
||||
"item_name": "Large Star Piece",
|
||||
"amount": 1,
|
||||
"item_value": 50000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Currency",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Currency",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Money",
|
||||
"Quality": 5,
|
||||
"Duration": 1,
|
||||
"trait": "Money",
|
||||
"quality": 5,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Star",
|
||||
"Quality": 5,
|
||||
"Duration": 1
|
||||
"trait": "Star",
|
||||
"quality": 5,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -195,90 +196,90 @@ kdl3_gifts = {
|
||||
|
||||
kdl3_trap_gifts = {
|
||||
0: {
|
||||
"ItemName": "Gooey Bag",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Gooey Bag",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Goo",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Goo",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Gel",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Gel",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
1: {
|
||||
"ItemName": "Slowness",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Slowness",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Slow",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Slow",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Slowness",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Slowness",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
2: {
|
||||
"ItemName": "Eject Ability",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Eject Ability",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Eject",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Eject",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Removal",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Removal",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
3: {
|
||||
"ItemName": "Bad Meal",
|
||||
"Amount": 1,
|
||||
"ItemValue": 10000,
|
||||
"Traits": [
|
||||
"item_name": "Bad Meal",
|
||||
"amount": 1,
|
||||
"item_value": 10000,
|
||||
"traits": [
|
||||
{
|
||||
"Trait": "Trap",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Trap",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Damage",
|
||||
"Quality": 1,
|
||||
"Duration": 1,
|
||||
"trait": "Damage",
|
||||
"quality": 1,
|
||||
"duration": 1,
|
||||
},
|
||||
{
|
||||
"Trait": "Food",
|
||||
"Quality": 1,
|
||||
"Duration": 1
|
||||
"trait": "Food",
|
||||
"quality": 1,
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -289,7 +289,7 @@ class KirbyFlavorPreset(Choice):
|
||||
option_lime = 12
|
||||
option_lavender = 13
|
||||
option_miku = 14
|
||||
option_custom = 15
|
||||
option_custom = -1
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -297,7 +297,7 @@ class KirbyFlavorPreset(Choice):
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(14)
|
||||
choice_list.remove(-1)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
@@ -347,7 +347,7 @@ class GooeyFlavorPreset(Choice):
|
||||
option_orange = 11
|
||||
option_lime = 12
|
||||
option_lavender = 13
|
||||
option_custom = 14
|
||||
option_custom = -1
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -355,7 +355,7 @@ class GooeyFlavorPreset(Choice):
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(14)
|
||||
choice_list.remove(-1)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
import settings
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
|
||||
from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
|
||||
get_gooey_palette
|
||||
@@ -475,8 +474,7 @@ def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little")
|
||||
if world.multiworld.players > 1 else bytes([0, 0]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little"))
|
||||
# don't write gifting for solo game, since there's no one to send anything to
|
||||
|
||||
@@ -594,9 +592,9 @@ def get_base_rom_bytes() -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: settings.Settings = settings.get_settings()
|
||||
from . import KDL3World
|
||||
if not file_name:
|
||||
file_name = options["kdl3_options"]["rom_file"]
|
||||
file_name = KDL3World.settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -220,7 +220,6 @@ To this day I still don't know if we inconvenienced the Mad Batter or not.
|
||||
Oh, hi #####
|
||||
People forgot I was playable in Hyrule Warriors
|
||||
Join our Discord. Or else.
|
||||
Also try Minecraft!
|
||||
I see you're finally awake...
|
||||
OwO
|
||||
This is Todd Howard, and today I'm pleased to announce... The Elder Scrolls V: Skyrim for the Nintendo Game Boy Color!
|
||||
@@ -281,7 +280,6 @@ Try Mario & Luigi Superstar Saga!
|
||||
Try MegaMan Battle Network 3!
|
||||
Try Meritous!
|
||||
Try The Messenger!
|
||||
Try Minecraft!
|
||||
Try Muse Dash!
|
||||
Try Noita!
|
||||
Try Ocarina of Time!
|
||||
|
||||
@@ -335,7 +335,9 @@ class LinksAwakeningWorld(World):
|
||||
start_item = next((item for item in start_items if opens_new_regions(item)), None)
|
||||
|
||||
if start_item:
|
||||
itempool.remove(start_item)
|
||||
# Make sure we're removing the same copy of the item that we're placing
|
||||
# (.remove checks __eq__, which could be a different copy, so we find the first index and use .pop)
|
||||
start_item = itempool.pop(itempool.index(start_item))
|
||||
start_loc.place_locked_item(start_item)
|
||||
else:
|
||||
logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import pkgutil
|
||||
|
||||
def load_data_file(*args) -> dict:
|
||||
fname = "/".join(["data", *args])
|
||||
return json.loads(pkgutil.get_data(__name__, fname).decode())
|
||||
|
||||
# For historical reasons, these values are different.
|
||||
# They remain different to ensure datapackage consistency.
|
||||
# Do not separate other games' location and item IDs like this.
|
||||
item_id_offset: int = 45000
|
||||
location_id_offset: int = 42000
|
||||
|
||||
item_info = load_data_file("items.json")
|
||||
item_name_to_id = {name: item_id_offset + index \
|
||||
for index, name in enumerate(item_info["all_items"])}
|
||||
item_name_to_id["Bee Trap"] = item_id_offset + 100 # historical reasons
|
||||
|
||||
location_info = load_data_file("locations.json")
|
||||
location_name_to_id = {name: location_id_offset + index \
|
||||
for index, name in enumerate(location_info["all_locations"])}
|
||||
|
||||
exclusion_info = load_data_file("excluded_locations.json")
|
||||
|
||||
region_info = load_data_file("regions.json")
|
||||
@@ -1,55 +0,0 @@
|
||||
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)]
|
||||
|
||||
# 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
|
||||
@@ -1,143 +0,0 @@
|
||||
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 = 114
|
||||
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 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
|
||||
death_link: DeathLink
|
||||
starting_items: StartingItems
|
||||
@@ -1,508 +0,0 @@
|
||||
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: # saddles, blaze rods, wither skulls
|
||||
return state.can_reach_region('Nether Fortress', player) and basic_combat(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)
|
||||
|
||||
|
||||
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 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),
|
||||
},
|
||||
"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
|
||||
"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: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(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.has("Fishing Rod", player) # Pufferfish, Nautilus Shells; spectral arrows
|
||||
and state.has("Archery", player)
|
||||
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: has_iron_ingots(world, state, player)
|
||||
and state.has("Bucket", player)
|
||||
and can_adventure(world, state, player),
|
||||
"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)
|
||||
and can_kill_ender_dragon(world, state, player)
|
||||
and can_kill_wither(world, state, player)
|
||||
and state.has("Fishing Rod", player)),
|
||||
"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: (can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player)
|
||||
and has_diamond_pickaxe(world, state, 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),
|
||||
"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)
|
||||
and has_gold_ingots(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2)
|
||||
and state.can_reach_region('The End', player)),
|
||||
# notch apple, chorus fruit
|
||||
"Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player),
|
||||
"Country Lode, Take Me Home": lambda state: state.can_reach_location("Hidden in the Depths", player)
|
||||
and has_gold_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: can_adventure(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.has("Progressive Resource Crafting", player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and can_brew_potions(world, state, player)
|
||||
and state.has("Bed", 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: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(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)
|
||||
),
|
||||
"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: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"Wax Off": lambda state: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"The Cutest Predator": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"The Healing Power of Friendship": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"Is It a Bird?": lambda state: has_spyglass(world, state, player)
|
||||
and can_adventure(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 (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, 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 basic_combat(world, state, 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),
|
||||
"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),
|
||||
"With Our Powers Combined!": lambda state: can_adventure(world, state, player)
|
||||
and state.has("Lead", 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)
|
||||
@@ -1,59 +0,0 @@
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
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]})")
|
||||
|
||||
# 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)
|
||||
@@ -1,203 +0,0 @@
|
||||
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 . import Constants
|
||||
from .Options import MinecraftOptions
|
||||
from .Structures import shuffle_structures
|
||||
from .ItemPool import build_item_pool, get_junk_item_names
|
||||
from .Rules import set_rules
|
||||
|
||||
client_version = 9
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
forge_directory: ForgeDirectory = ForgeDirectory("Minecraft NeoForge server")
|
||||
max_heap_size: str = "2G"
|
||||
release_channel: ReleaseChannel = ReleaseChannel("release")
|
||||
java: JavaExecutable = JavaExecutable("")
|
||||
|
||||
|
||||
class MinecraftWebWorld(WebWorld):
|
||||
theme = "jungle"
|
||||
bug_report_page = "https://github.com/KonoTyran/Minecraft_AP_Randomizer/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",
|
||||
["Kono Tyran"]
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def _get_mc_data(self) -> Dict[str, Any]:
|
||||
exits = [connection[0] for connection in Constants.region_info["default_connections"]]
|
||||
return {
|
||||
'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),
|
||||
'starting_items': json.dumps(self.options.starting_items.value),
|
||||
'race': self.multiworld.is_race,
|
||||
}
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_class = ItemClassification.filler
|
||||
if name in Constants.item_info["progression_items"]:
|
||||
item_class = ItemClassification.progression
|
||||
elif name in Constants.item_info["useful_items"]:
|
||||
item_class = ItemClassification.useful
|
||||
elif 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 = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc"
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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'))
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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!"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"all_items": [
|
||||
"Archery",
|
||||
"Progressive Resource Crafting",
|
||||
"Resource Blocks",
|
||||
"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"
|
||||
],
|
||||
"progression_items": [
|
||||
"Archery",
|
||||
"Progressive Resource Crafting",
|
||||
"Resource Blocks",
|
||||
"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"
|
||||
],
|
||||
"useful_items": [
|
||||
"Sharpness III Book",
|
||||
"Looting III Book",
|
||||
"Infinity Book"
|
||||
],
|
||||
"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,
|
||||
"Spyglass": 1,
|
||||
"Lead": 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
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Monster Hunter",
|
||||
"Adventuring Time",
|
||||
"A Seedy Place",
|
||||
"Husbandry",
|
||||
"Bee Our Guest",
|
||||
"Diamonds!",
|
||||
"A Throwaway Joke",
|
||||
"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",
|
||||
"Librarian",
|
||||
"Wax On",
|
||||
"Wax Off",
|
||||
"The Cutest Predator",
|
||||
"The Healing Power of Friendship",
|
||||
"Is It a Bird?",
|
||||
"Surge Protector",
|
||||
"Light as a Rabbit",
|
||||
"Glow and Behold!",
|
||||
"Whatever Floats Your Goat!",
|
||||
"Caves & Cliffs",
|
||||
"Sound of Music",
|
||||
"Bukkit Bukkit",
|
||||
"It Spreads",
|
||||
"Sneak 100",
|
||||
"When the Squad Hops into Town"
|
||||
],
|
||||
"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",
|
||||
"Serious Dedication",
|
||||
"Hidden in the Depths",
|
||||
"Subspace Bubble",
|
||||
"Country Lode, Take Me Home",
|
||||
"Uneasy Alliance",
|
||||
"Cover Me in Debris",
|
||||
"Is It a Balloon?",
|
||||
"Feels like home",
|
||||
"With Our Powers Combined!"
|
||||
],
|
||||
"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": [
|
||||
"Postmortal",
|
||||
"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",
|
||||
"Overkill"
|
||||
],
|
||||
"Pillager Outpost": [
|
||||
"Who's the Pillager Now?",
|
||||
"Voluntary Exile",
|
||||
"Birthday Song",
|
||||
"You've Got a Friend in Me"
|
||||
],
|
||||
"Bastion Remnant": [
|
||||
"War Pigs",
|
||||
"Those Were the Days",
|
||||
"Overpowered"
|
||||
],
|
||||
"End City": [
|
||||
"Great View From Up Here",
|
||||
"The City at the End of the Game",
|
||||
"Sky's the Limit"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"regions": [
|
||||
["Menu", ["New World"]],
|
||||
["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2"]],
|
||||
["The Nether", ["Nether Structure 1", "Nether Structure 2"]],
|
||||
["The End", ["The End Structure"]],
|
||||
["Village", []],
|
||||
["Pillager Outpost", []],
|
||||
["Nether Fortress", []],
|
||||
["Bastion Remnant", []],
|
||||
["End City", []]
|
||||
],
|
||||
"mandatory_connections": [
|
||||
["New World", "Overworld"],
|
||||
["Nether Portal", "The Nether"],
|
||||
["End Portal", "The End"]
|
||||
],
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
# 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 achievements. 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 achievements. Opening the advancements menu
|
||||
in-game by pressing "L" will display outstanding achievements.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
* Spyglass
|
||||
* Lead
|
||||
* Progressive Weapons
|
||||
* Tier I
|
||||
* Stone Sword
|
||||
* Stone Axe
|
||||
* Tier II
|
||||
* Iron Sword
|
||||
* Iron Axe
|
||||
* Tier III
|
||||
* Diamond Sword
|
||||
* Diamond Axe
|
||||
* Progessive Tools
|
||||
* Tier I
|
||||
* Stone Pickaxe
|
||||
* Stone Shovel
|
||||
* Stone 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
|
||||
* 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 Block
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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.**
|
||||
|
||||
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 forge 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 <AP-Address> (Port) (Password)` where `<AP-Address>` 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 `<AP-Address>` and `(Port)`.
|
||||
`(Password)` is only required if the Archipelago server you are using has a password set.
|
||||
|
||||
### 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 forge server. To start the game once
|
||||
everyone is ready use the command `/start`.
|
||||
|
||||
## Non-Windows Installation
|
||||
|
||||
The Minecraft Client will install forge and the mod for other operating systems but Java has to be provided by the
|
||||
user. Head to [minecraft_versions.json on the MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
|
||||
to see which java version is required. New installations will default to the topmost "release" version.
|
||||
- Install the matching Amazon Corretto JDK
|
||||
- see [Manual Installation Software Links](#manual-installation-software-links)
|
||||
- or package manager provided by your OS / distribution
|
||||
- Open your `host.yaml` and add the path to your Java below the `minecraft_options` key
|
||||
- ` java: "path/to/java-xx-amazon-corretto/bin/java"`
|
||||
- Run the Minecraft Client and select your .apmc file
|
||||
|
||||
## Full Manual Installation
|
||||
|
||||
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
|
||||
Support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
|
||||
the following links are the versions of the software we use.
|
||||
|
||||
### Manual Installation Software Links
|
||||
|
||||
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/)
|
||||
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **DO NOT INSTALL THIS ON YOUR CLIENT**
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- pick the matching version and select "Downloads" on the left
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# 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 <AP-Address> (Port) (<Password>)` donde `<AP-Address>` es la dirección del
|
||||
servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281.
|
||||
`(<Password>)`
|
||||
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/KonoTyran/Minecraft_AP_Randomizer/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
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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 <AP-Address> (Port) (Password)` où `<AP-Address>` 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 `<AP-Address>` 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/KonoTyran/Minecraft_AP_Randomizer/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/KonoTyran/Minecraft_AP_Randomizer/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
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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/KonoTyran/Minecraft_AP_Randomizer/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 <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
|
||||
Archipelago-servern. `(<Lösenord>)` ä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.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
requests >= 2.28.1 # used by client
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
from . 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']],
|
||||
|
||||
])
|
||||
@@ -1,49 +0,0 @@
|
||||
from . 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 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
|
||||
@@ -1,33 +0,0 @@
|
||||
from test.bases import TestBase, WorldTestBase
|
||||
from .. import MinecraftWorld, MinecraftOptions
|
||||
|
||||
|
||||
class MCTestBase(WorldTestBase, TestBase):
|
||||
game = "Minecraft"
|
||||
player: 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)
|
||||
|
||||
@@ -185,7 +185,7 @@ class TextArchive:
|
||||
# As far as I know, this should literally not be possible.
|
||||
# Every script I've looked at has dozens of unused indices, so finding 9 (8 plus one "ending" script)
|
||||
# should be no problem. We re-use these so we don't have to worry about an area getting tons of these
|
||||
raise AssertionError("Error in generation -- not enough room for progressive undernet in archive "+self.startOffset)
|
||||
raise AssertionError(f"Error in generation -- not enough room for progressive undernet in archive {self.startOffset} ({hex(self.startOffset)})")
|
||||
for i in range(9): # There are 8 progressive undernet ranks
|
||||
new_script_index = self.unused_indices[i]
|
||||
new_script = ArchiveScript(new_script_index, generate_progressive_undernet(i, self.unused_indices[i+1]))
|
||||
@@ -319,15 +319,16 @@ class MMBN3DeltaPatch(APDeltaPatch):
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
bn3_options = options.get("mmbn3_options", None)
|
||||
from worlds.mmbn3 import MMBN3World
|
||||
bn3_options = MMBN3World.settings
|
||||
|
||||
if bn3_options is None:
|
||||
file_name = "Mega Man Battle Network 3 - Blue Version (USA).gba"
|
||||
else:
|
||||
file_name = bn3_options["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import settings
|
||||
import typing
|
||||
import threading
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, Region, Entrance, \
|
||||
LocationProgressType
|
||||
@@ -16,7 +15,7 @@ from .Options import MMBN3Options
|
||||
from .Regions import regions, RegionName
|
||||
from .Names.ItemName import ItemName
|
||||
from .Names.LocationName import LocationName
|
||||
from worlds.generic.Rules import add_item_rule, add_rule
|
||||
from worlds.generic.Rules import add_item_rule, add_rule, forbid_item
|
||||
|
||||
|
||||
class MMBN3Settings(settings.Group):
|
||||
@@ -26,8 +25,15 @@ class MMBN3Settings(settings.Group):
|
||||
description = "MMBN3 ROM File"
|
||||
md5s = [MMBN3DeltaPatch.hash]
|
||||
|
||||
class RomStart(str):
|
||||
"""
|
||||
Set this to false to never autostart a rom (such as after patching),
|
||||
true for operating system default program
|
||||
Alternatively, a path to a program to open the .gba file with
|
||||
"""
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: bool = True
|
||||
rom_start: RomStart | bool = True
|
||||
|
||||
|
||||
class MMBN3Web(WebWorld):
|
||||
@@ -203,134 +209,134 @@ class MMBN3World(World):
|
||||
|
||||
# Set WWW ID requirements
|
||||
def has_www_id(state): return state.has(ItemName.WWW_ID, self.player)
|
||||
add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.SciLab_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.multiworld.get_location(LocationName.Undernet_1_WWW_BMD, self.player), has_www_id)
|
||||
add_rule(self.get_location(LocationName.ACDC_1_PMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.SciLab_1_WWW_BMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_WWW_BMD), has_www_id)
|
||||
add_rule(self.get_location(LocationName.Undernet_1_WWW_BMD), has_www_id)
|
||||
|
||||
# Set Press Program requirements
|
||||
def has_press(state): return state.has(ItemName.Press, self.player)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_2_Upper_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_2_East_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Hades_South_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Secret_3_BugFrag_BMD, self.player), has_press)
|
||||
add_rule(self.multiworld.get_location(LocationName.Secret_3_Island_BMD, self.player), has_press)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_PMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Yoka_2_Upper_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Beach_2_East_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Hades_South_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Secret_3_BugFrag_BMD), has_press)
|
||||
add_rule(self.get_location(LocationName.Secret_3_Island_BMD), has_press)
|
||||
|
||||
# Set Purple Mystery Data Unlocker access
|
||||
def can_unlock(state): return state.can_reach_region(RegionName.SciLab_Overworld, self.player) or \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) or \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) or \
|
||||
state.has(ItemName.Unlocker, self.player, 8) # There are 8 PMDs that aren't in one of the above areas
|
||||
add_rule(self.multiworld.get_location(LocationName.ACDC_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Yoka_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_1_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Undernet_7_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Mayls_HP_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.SciLab_Dads_Computer_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Zoo_Panda_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Security_Panel_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Beach_DNN_Main_Console_PMD, self.player), can_unlock)
|
||||
add_rule(self.multiworld.get_location(LocationName.Tamakos_HP_PMD, self.player), can_unlock)
|
||||
add_rule(self.get_location(LocationName.ACDC_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Yoka_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_1_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Undernet_7_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Mayls_HP_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.SciLab_Dads_Computer_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Zoo_Panda_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_DNN_Security_Panel_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Beach_DNN_Main_Console_PMD), can_unlock)
|
||||
add_rule(self.get_location(LocationName.Tamakos_HP_PMD), can_unlock)
|
||||
|
||||
# Set Job additional area access
|
||||
self.multiworld.get_location(LocationName.Please_deliver_this, self.player).access_rule = \
|
||||
self.get_location(LocationName.Please_deliver_this).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.My_Navi_is_sick, self.player).access_rule =\
|
||||
self.get_location(LocationName.My_Navi_is_sick).access_rule =\
|
||||
lambda state: \
|
||||
state.has(ItemName.Recov30_star, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_me_with_my_son, self.player).access_rule =\
|
||||
self.get_location(LocationName.Help_me_with_my_son).access_rule =\
|
||||
lambda state:\
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Transmission_error, self.player).access_rule = \
|
||||
self.get_location(LocationName.Transmission_error).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Chip_Prices, self.player).access_rule = \
|
||||
self.get_location(LocationName.Chip_Prices).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Im_broke, self.player).access_rule = \
|
||||
self.get_location(LocationName.Im_broke).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Rare_chips_for_cheap, self.player).access_rule = \
|
||||
self.get_location(LocationName.Rare_chips_for_cheap).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Be_my_boyfriend, self.player).access_rule =\
|
||||
self.get_location(LocationName.Be_my_boyfriend).access_rule =\
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Will_you_deliver, self.player).access_rule=\
|
||||
self.get_location(LocationName.Will_you_deliver).access_rule=\
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Somebody_please_help, self.player).access_rule = \
|
||||
self.get_location(LocationName.Somebody_please_help).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Looking_for_condor, self.player).access_rule = \
|
||||
self.get_location(LocationName.Looking_for_condor).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_rehab).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab_bonus, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_rehab_bonus).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Old_Master, self.player).access_rule = \
|
||||
self.get_location(LocationName.Old_Master).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Catching_gang_members, self.player).access_rule = \
|
||||
self.get_location(LocationName.Catching_gang_members).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \
|
||||
state.has(ItemName.Press, self.player)
|
||||
self.multiworld.get_location(LocationName.Please_adopt_a_virus, self.player).access_rule = \
|
||||
self.get_location(LocationName.Please_adopt_a_virus).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Legendary_Tomes, self.player).access_rule = \
|
||||
self.get_location(LocationName.Legendary_Tomes).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Undernet, self.player) and \
|
||||
state.can_reach_region(RegionName.Deep_Undernet, self.player) and \
|
||||
state.has_all({ItemName.Press, ItemName.Magnum1_A}, self.player)
|
||||
self.multiworld.get_location(LocationName.Legendary_Tomes_Treasure, self.player).access_rule = \
|
||||
self.get_location(LocationName.Legendary_Tomes_Treasure).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_location(LocationName.Legendary_Tomes, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_First_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_First_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Second_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Second_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Third_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Third_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Fourth_Child, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Fourth_Child).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Hide_and_seek_Completion, self.player).access_rule = \
|
||||
self.get_location(LocationName.Hide_and_seek_Completion).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Yoka_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Finding_the_blue_Navi, self.player).access_rule = \
|
||||
self.get_location(LocationName.Finding_the_blue_Navi).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Undernet, self.player)
|
||||
self.multiworld.get_location(LocationName.Give_your_support, self.player).access_rule = \
|
||||
self.get_location(LocationName.Give_your_support).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Stamp_collecting, self.player).access_rule = \
|
||||
self.get_location(LocationName.Stamp_collecting).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.SciLab_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Yoka_Cyberworld, self.player) and \
|
||||
state.can_reach_region(RegionName.Beach_Cyberworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_a_will, self.player).access_rule = \
|
||||
self.get_location(LocationName.Help_with_a_will).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
state.can_reach_region(RegionName.ACDC_Cyberworld, self.player) and \
|
||||
@@ -340,100 +346,115 @@ class MMBN3World(World):
|
||||
state.can_reach_region(RegionName.Undernet, self.player)
|
||||
|
||||
# Set Trade quests
|
||||
self.multiworld.get_location(LocationName.ACDC_SonicWav_W_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_SonicWav_W_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.SonicWav_W, self.player)
|
||||
self.multiworld.get_location(LocationName.ACDC_Bubbler_C_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_Bubbler_C_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Bubbler_C, self.player)
|
||||
self.multiworld.get_location(LocationName.ACDC_Recov120_S_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.ACDC_Recov120_S_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Recov120_S, self.player)
|
||||
self.multiworld.get_location(LocationName.SciLab_Shake1_S_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.SciLab_Shake1_S_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Shake1_S, self.player)
|
||||
self.multiworld.get_location(LocationName.Yoka_FireSwrd_P_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Yoka_FireSwrd_P_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.FireSwrd_P, self.player)
|
||||
self.multiworld.get_location(LocationName.Hospital_DynaWav_V_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Hospital_DynaWav_V_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.DynaWave_V, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_WideSwrd_C_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.WideSwrd_C, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_HoleMetr_H_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.HoleMetr_H, self.player)
|
||||
self.multiworld.get_location(LocationName.Beach_DNN_Shadow_J_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Beach_DNN_Shadow_J_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.Shadow_J, self.player)
|
||||
self.multiworld.get_location(LocationName.Hades_GrabBack_K_Trade, self.player).access_rule =\
|
||||
self.get_location(LocationName.Hades_GrabBack_K_Trade).access_rule =\
|
||||
lambda state: state.has(ItemName.GrabBack_K, self.player)
|
||||
|
||||
# Set Number Traders
|
||||
|
||||
# The first 8 are considered cheap enough to grind for in ACDC. Protip: Try grinding in the tank
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_09, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_09).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_10, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_10).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_11, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_11).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_12, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_12).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_13, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_13).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_14, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_14).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_15, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_15).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_16, self.player).access_rule = \
|
||||
self.get_location(LocationName.Numberman_Code_16).access_rule = \
|
||||
lambda state: self.explore_score(state) > 2
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_17, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_17).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_18, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_18).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_19, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_19).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_20, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_20).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_21, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_21).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_22, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_22).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_23, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_23).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_24, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_24).access_rule =\
|
||||
lambda state: self.explore_score(state) > 4
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_25, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_25).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_26, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_26).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_27, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_27).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_28, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_28).access_rule =\
|
||||
lambda state: self.explore_score(state) > 8
|
||||
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_29, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_29).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_30, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_30).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
self.multiworld.get_location(LocationName.Numberman_Code_31, self.player).access_rule =\
|
||||
self.get_location(LocationName.Numberman_Code_31).access_rule =\
|
||||
lambda state: self.explore_score(state) > 10
|
||||
|
||||
#miscellaneous locations with extra requirements
|
||||
add_rule(self.multiworld.get_location(LocationName.Comedian, self.player),
|
||||
add_rule(self.get_location(LocationName.Comedian),
|
||||
lambda state: state.has(ItemName.Humor, self.player))
|
||||
add_rule(self.multiworld.get_location(LocationName.Villain, self.player),
|
||||
add_rule(self.get_location(LocationName.Villain),
|
||||
lambda state: state.has(ItemName.BlckMnd, self.player))
|
||||
def not_undernet(item): return item.code != item_table[ItemName.Progressive_Undernet_Rank].code or item.player != self.player
|
||||
self.multiworld.get_location(LocationName.WWW_1_Central_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_1_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_2_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_2_Northwest_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_3_East_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_3_North_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_4_Northwest_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_4_Central_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Wall_BMD, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Control_Room_1_Screen, self.player).item_rule = not_undernet
|
||||
self.multiworld.get_location(LocationName.WWW_Wilys_Desk, self.player).item_rule = not_undernet
|
||||
forbid_item(self.get_location(LocationName.WWW_1_Central_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_1_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_2_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_2_Northwest_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_3_East_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_3_North_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_4_Northwest_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_4_Central_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Wall_BMD),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Control_Room_1_Screen),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
forbid_item(self.get_location(LocationName.WWW_Wilys_Desk),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
|
||||
# I have no fuckin clue why this specific location shits the bed on a progressive undernet rank.
|
||||
# If you ever figure it out I will buy you a pizza.
|
||||
forbid_item(self.get_location(LocationName.Chocolate_Shop_07),
|
||||
ItemName.Progressive_Undernet_Rank, self.player)
|
||||
|
||||
# place "Victory" at "Final Boss" and set collection as win condition
|
||||
self.multiworld.get_location(LocationName.Alpha_Defeated, self.player) \
|
||||
self.get_location(LocationName.Alpha_Defeated) \
|
||||
.place_locked_item(self.create_event(ItemName.Victory))
|
||||
self.multiworld.completion_condition[self.player] = \
|
||||
lambda state: state.has(ItemName.Victory, self.player)
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import OptionError
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
from .Names import LocationName
|
||||
@@ -99,8 +100,9 @@ def get_gate_bosses(world: World):
|
||||
pass
|
||||
|
||||
if boss in plando_bosses:
|
||||
# TODO: Raise error here. Duplicates not allowed
|
||||
pass
|
||||
raise OptionError(f"Invalid input for option `plando_bosses`: "
|
||||
f"No Duplicate Bosses permitted ({boss}) - for "
|
||||
f"{world.player_name}")
|
||||
|
||||
plando_bosses[boss_num] = boss
|
||||
|
||||
@@ -108,13 +110,14 @@ def get_gate_bosses(world: World):
|
||||
available_bosses.remove(boss)
|
||||
|
||||
for x in range(world.options.number_of_level_gates):
|
||||
if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
available_bosses.extend(gate_bosses_with_requirements_table)
|
||||
if (10 not in selected_bosses) and (king_boom_boo not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
|
||||
available_bosses.extend(gate_bosses_with_requirements_table.keys())
|
||||
world.random.shuffle(available_bosses)
|
||||
|
||||
chosen_boss = available_bosses[0]
|
||||
if plando_bosses[x] != "None":
|
||||
available_bosses.append(plando_bosses[x])
|
||||
if plando_bosses[x] not in available_bosses:
|
||||
available_bosses.append(plando_bosses[x])
|
||||
chosen_boss = plando_bosses[x]
|
||||
|
||||
selected_bosses.append(all_gate_bosses_table[chosen_boss])
|
||||
|
||||
@@ -324,7 +324,8 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
|
||||
add_rule_safe(multiworld, LocationName.iron_gate_5, player,
|
||||
lambda state: state.has(ItemName.eggman_large_cannon, player))
|
||||
add_rule_safe(multiworld, LocationName.dry_lagoon_5, player,
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
lambda state: state.has(ItemName.rouge_pick_nails, player) and
|
||||
state.has(ItemName.rouge_treasure_scope, player))
|
||||
add_rule_safe(multiworld, LocationName.sand_ocean_5, player,
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule_safe(multiworld, LocationName.egg_quarters_5, player,
|
||||
@@ -407,8 +408,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
|
||||
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
|
||||
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
|
||||
state.has(ItemName.eggman_jet_engine, player))
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player) and
|
||||
@@ -1402,8 +1402,6 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
|
||||
state.has(ItemName.eggman_large_cannon, player)))
|
||||
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
|
||||
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
|
||||
state.has(ItemName.rouge_treasure_scope, player)))
|
||||
@@ -1724,6 +1722,9 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.white_jungle_itembox_8, player),
|
||||
lambda state: state.has(ItemName.shadow_air_shoes, player))
|
||||
add_rule(multiworld.get_location(LocationName.sky_rail_itembox_8, player),
|
||||
lambda state: (state.has(ItemName.shadow_air_shoes, player) and
|
||||
state.has(ItemName.shadow_mystic_melody, player)))
|
||||
add_rule(multiworld.get_location(LocationName.mad_space_itembox_8, player),
|
||||
lambda state: state.has(ItemName.rouge_iron_boots, player))
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_itembox_8, player),
|
||||
@@ -2308,8 +2309,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
|
||||
lambda state: state.has(ItemName.tails_booster, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
|
||||
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
|
||||
state.has(ItemName.eggman_jet_engine, player))
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player) and
|
||||
@@ -2980,8 +2980,6 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
|
||||
state.has(ItemName.eggman_jet_engine, player)))
|
||||
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
|
||||
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
|
||||
state.has(ItemName.rouge_treasure_scope, player)))
|
||||
@@ -3593,8 +3591,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
lambda state: state.has(ItemName.tails_booster, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
|
||||
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
|
||||
state.has(ItemName.eggman_jet_engine, player))
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player) and
|
||||
@@ -3643,9 +3640,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player),
|
||||
lambda state: state.has(ItemName.tails_bazooka, player))
|
||||
add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player),
|
||||
@@ -3771,10 +3765,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player) and
|
||||
state.has(ItemName.knuckles_hammer_gloves, player))
|
||||
|
||||
# Animal Upgrade Requirements
|
||||
if world.options.animalsanity:
|
||||
add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player),
|
||||
@@ -3839,8 +3829,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player),
|
||||
lambda state: state.has(ItemName.rouge_pick_nails, player) and
|
||||
state.has(ItemName.rouge_iron_boots, player))
|
||||
lambda state: state.has(ItemName.rouge_iron_boots, player))
|
||||
add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
@@ -3976,8 +3965,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
state.has(ItemName.tails_bazooka, player))
|
||||
add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player),
|
||||
lambda state: state.has(ItemName.sonic_flame_ring, player))
|
||||
add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player),
|
||||
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player),
|
||||
lambda state: state.has(ItemName.shadow_flame_ring, player))
|
||||
@@ -4035,8 +4022,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from .Constants import *
|
||||
|
||||
def launch_client(*args: str):
|
||||
from .Client import launch
|
||||
launch_subprocess(launch(*args), name=CLIENT_NAME)
|
||||
launch_subprocess(launch, name=CLIENT_NAME, args=args)
|
||||
|
||||
|
||||
components.append(
|
||||
|
||||
@@ -57,7 +57,7 @@ Ein Pop-Up erscheint, das das/die erhaltene(n) Item(s) und eventuell weitere Inf
|
||||
|
||||
Hier ist ein Spicker für die Englischarbeit (bloß nicht dem Lehrer zeigen):
|
||||
|
||||

|
||||

|
||||
|
||||
## Kann ich auch weitere Mods neben dem AP Client installieren?
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ A pop-up will show, which item(s) were received, with additional information on
|
||||
|
||||
Here's a cheat sheet:
|
||||
|
||||

|
||||

|
||||
|
||||
## Can I use other mods alongside the AP client?
|
||||
|
||||
|
||||
@@ -261,13 +261,13 @@ class ShiversWorld(World):
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
elif self.options.full_pots == "complete":
|
||||
return [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||
data.type == ItemType.POT_COMPLETE_DUPLICATE]
|
||||
else:
|
||||
pool = []
|
||||
pieces = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
complete = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||
data.type == ItemType.POT_COMPLETE_DUPLICATE]
|
||||
for i in range(10):
|
||||
if self.pot_completed_list[i] == 0:
|
||||
pool.append(pieces[i])
|
||||
|
||||
@@ -271,11 +271,11 @@ solar_essence = BundleItem(Loot.solar_essence)
|
||||
void_essence = BundleItem(Loot.void_essence)
|
||||
|
||||
petrified_slime = BundleItem(Mineral.petrified_slime)
|
||||
blue_slime_egg = BundleItem(Loot.blue_slime_egg)
|
||||
red_slime_egg = BundleItem(Loot.red_slime_egg)
|
||||
purple_slime_egg = BundleItem(Loot.purple_slime_egg)
|
||||
green_slime_egg = BundleItem(Loot.green_slime_egg)
|
||||
tiger_slime_egg = BundleItem(Loot.tiger_slime_egg, source=BundleItem.Sources.island)
|
||||
blue_slime_egg = BundleItem(AnimalProduct.slime_egg_blue)
|
||||
red_slime_egg = BundleItem(AnimalProduct.slime_egg_red)
|
||||
purple_slime_egg = BundleItem(AnimalProduct.slime_egg_purple)
|
||||
green_slime_egg = BundleItem(AnimalProduct.slime_egg_green)
|
||||
tiger_slime_egg = BundleItem(AnimalProduct.slime_egg_tiger, source=BundleItem.Sources.island)
|
||||
|
||||
cherry_bomb = BundleItem(Bomb.cherry_bomb, 5)
|
||||
bomb = BundleItem(Bomb.bomb, 2)
|
||||
|
||||
@@ -168,15 +168,16 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
|
||||
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
|
||||
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
|
||||
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
|
||||
AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
|
||||
AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
|
||||
AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
|
||||
AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
|
||||
AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
|
||||
AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
|
||||
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
|
||||
AnimalProduct.slime_egg_tiger: self.can_fish_pond(Fish.lionfish, *(Forageable.ginger, Fruit.pineapple, Fruit.mango)) & self.time.has_lived_months(12) &
|
||||
self.building.has_building(Building.slime_hutch) & self.monster.can_kill(Monster.tiger_slime),
|
||||
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
|
||||
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
|
||||
AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet...
|
||||
AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
|
||||
AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
|
||||
@@ -233,7 +234,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), #
|
||||
Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton),
|
||||
Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
|
||||
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
|
||||
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe) & self.received("Open Professor Snail Cave"),
|
||||
Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
|
||||
Fossil.fossilized_spine: self.fishing.can_fish_at(Region.dig_site),
|
||||
Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper),
|
||||
@@ -288,9 +289,9 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
|
||||
MetalBar.radioactive: self.can_smelt(Ore.radioactive),
|
||||
Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
|
||||
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
|
||||
Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
|
||||
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
|
||||
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
|
||||
Ore.iridium: self.count(2, *(self.mine.can_mine_in_the_skull_cavern(), self.can_fish_pond(Fish.super_cucumber), self.tool.has_tool(Tool.pan, ToolMaterial.iridium))),
|
||||
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
|
||||
Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
|
||||
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
@@ -381,5 +382,8 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
def can_use_obelisk(self, obelisk: str) -> StardewRule:
|
||||
return self.region.can_reach(Region.farm) & self.received(obelisk)
|
||||
|
||||
def can_fish_pond(self, fish: str) -> StardewRule:
|
||||
return self.building.has_building(Building.fish_pond) & self.has(fish)
|
||||
def can_fish_pond(self, fish: str, *items: str) -> StardewRule:
|
||||
rule = self.building.has_building(Building.fish_pond) & self.has(fish)
|
||||
if items:
|
||||
rule = rule & self.has_all(*items)
|
||||
return rule
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
class Loot:
|
||||
blue_slime_egg = "Blue Slime Egg"
|
||||
red_slime_egg = "Red Slime Egg"
|
||||
purple_slime_egg = "Purple Slime Egg"
|
||||
green_slime_egg = "Green Slime Egg"
|
||||
tiger_slime_egg = "Tiger Slime Egg"
|
||||
slime = "Slime"
|
||||
bug_meat = "Bug Meat"
|
||||
bat_wing = "Bat Wing"
|
||||
|
||||
@@ -11,7 +11,7 @@ class EntranceRandomizationAssertMixin:
|
||||
non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag]
|
||||
|
||||
for non_progression_connections in non_progression_connections:
|
||||
with self.subTest(connection=non_progression_connections):
|
||||
with self.subTest(connection=non_progression_connections.name):
|
||||
self.assert_can_reach_entrance(non_progression_connections.name)
|
||||
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ from ...regions.regions import create_all_regions, create_all_connections
|
||||
class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
|
||||
def test_region_exits_lead_somewhere(self):
|
||||
for region in vanilla_data.regions_with_ginger_island_by_name.values():
|
||||
with self.subTest(region=region):
|
||||
with self.subTest(region=region.name):
|
||||
for exit_ in region.exits:
|
||||
self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name,
|
||||
f"{region.name} is leading to {exit_} but it does not exist.")
|
||||
|
||||
def test_connection_lead_somewhere(self):
|
||||
for connection in vanilla_data.connections_with_ginger_island_by_name.values():
|
||||
with self.subTest(connection=connection):
|
||||
with self.subTest(connection=connection.name):
|
||||
self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name,
|
||||
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
||||
|
||||
@@ -27,14 +27,14 @@ class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
|
||||
class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase):
|
||||
def test_region_exits_lead_somewhere(self):
|
||||
for region in vanilla_data.regions_without_ginger_island_by_name.values():
|
||||
with self.subTest(region=region):
|
||||
with self.subTest(region=region.name):
|
||||
for exit_ in region.exits:
|
||||
self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name,
|
||||
f"{region.name} is leading to {exit_} but it does not exist.")
|
||||
|
||||
def test_connection_lead_somewhere(self):
|
||||
for connection in vanilla_data.connections_without_ginger_island_by_name.values():
|
||||
with self.subTest(connection=connection):
|
||||
with self.subTest(connection=connection.name):
|
||||
self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name,
|
||||
f"{connection.name} is leading to {connection.destination} but it does not exist.")
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
|
||||
SkillProgression.internal_name: SkillProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_progressive,
|
||||
Fishsanity.internal_name: Fishsanity.option_all,
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
|
||||
@@ -18,7 +18,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
|
||||
fish_and_items = {
|
||||
Fish.crimsonfish: ["Beach Bridge"],
|
||||
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
|
||||
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
|
||||
Fish.woodskip: ["Progressive Axe", "Progressive Axe", "Progressive Weapon"], # For the ores to get the axe upgrades
|
||||
Fish.mutant_carp: ["Rusty Key"],
|
||||
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
|
||||
Fish.lionfish: ["Boat Repair"],
|
||||
@@ -26,8 +26,8 @@ class TestNeedRegionToCatchFish(SVTestBase):
|
||||
Fish.stingray: ["Boat Repair", "Island Resort"],
|
||||
Fish.ghostfish: ["Progressive Weapon"],
|
||||
Fish.stonefish: ["Progressive Weapon"],
|
||||
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe"],
|
||||
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe", "Progressive Pickaxe"],
|
||||
Fish.sandfish: ["Bus Repair"],
|
||||
Fish.scorpion_carp: ["Desert Obelisk"],
|
||||
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
|
||||
@@ -37,6 +37,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
|
||||
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
}
|
||||
self.collect("Progressive Fishing Rod", 4)
|
||||
self.original_state = self.multiworld.state.copy()
|
||||
for fish in fish_and_items:
|
||||
with self.subTest(f"Region rules for {fish}"):
|
||||
|
||||
Reference in New Issue
Block a user