mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
41 Commits
Exempt-Med
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e5d7f872 | ||
|
|
e68b1ad428 | ||
|
|
072e2ece15 | ||
|
|
11130037fe | ||
|
|
ba66ef14cc | ||
|
|
8aacc23882 | ||
|
|
03e5fd3dae | ||
|
|
da52598c08 | ||
|
|
52389731eb | ||
|
|
21864f6f95 | ||
|
|
00f8625280 | ||
|
|
c34e29c712 | ||
|
|
e0ae3359f1 | ||
|
|
c2666bacd7 | ||
|
|
4eefd9c3ce | ||
|
|
211456242e | ||
|
|
6f244c4661 | ||
|
|
47bf6d724b | ||
|
|
5c710ad032 | ||
|
|
dda5a05cbb | ||
|
|
e0a63e0290 | ||
|
|
9246659589 | ||
|
|
377cdb84b4 | ||
|
|
0e759f25fd | ||
|
|
b408bb4f6e | ||
|
|
1356479415 | ||
|
|
ec5b4e704f | ||
|
|
aa9e617510 | ||
|
|
ecb739ce96 | ||
|
|
3b72140435 | ||
|
|
27a6770569 | ||
|
|
2ff611167a | ||
|
|
e83e178b63 | ||
|
|
068a757373 | ||
|
|
0ad4527719 | ||
|
|
8c6327d024 | ||
|
|
aecbb2ab02 | ||
|
|
52b11083fe | ||
|
|
a8c87ce54b | ||
|
|
ddb3240591 | ||
|
|
f25ef639f2 |
2
.github/workflows/label-pull-requests.yml
vendored
2
.github/workflows/label-pull-requests.yml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
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)
|
||||
|
||||
@@ -196,7 +196,8 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
|
||||
def launch(exe, in_terminal=False):
|
||||
if in_terminal:
|
||||
if is_windows:
|
||||
subprocess.Popen(['start', *exe], shell=True)
|
||||
# intentionally using a window title with a space so it gets quoted and treated as a title
|
||||
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
|
||||
return
|
||||
elif is_linux:
|
||||
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
|
||||
|
||||
@@ -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
|
||||
@@ -291,7 +266,7 @@ like entrance randomization in logic.
|
||||
|
||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L310-L311)),
|
||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
||||
|
||||
### Entrances
|
||||
@@ -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)`
|
||||
@@ -501,7 +533,7 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
called to modify item placement before, during, and after the regular fill process; all finishing before
|
||||
`generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
|
||||
are any items that need to be filled this way, but need to be in state while you fill other items, they can be
|
||||
returned from `get_prefill_items`.
|
||||
returned from `get_pre_fill_items`.
|
||||
* `generate_output(self, output_directory: str)`
|
||||
creates the output files if there is output to be generated. When this is called,
|
||||
`self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the
|
||||
|
||||
@@ -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: "";
|
||||
|
||||
178
kvui.py
178
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,32 @@ 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.add_widget(self.tabs)
|
||||
tab_container = MDGridLayout(size_hint_y=1, cols=1)
|
||||
tab_container.add_widget(self.tabs)
|
||||
tab_container.add_widget(self.screens)
|
||||
self.main_area_container.add_widget(tab_container)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
@@ -974,25 +962,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} " \
|
||||
|
||||
@@ -295,7 +295,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
"""
|
||||
|
||||
required_server_version: Tuple[int, int, int] = (0, 5, 0)
|
||||
required_server_version: Tuple[int, int, int] = (0, 6, 2)
|
||||
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
||||
|
||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
||||
@@ -382,7 +382,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_pre_fill_items`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,16 +20,17 @@ A short period after receiving an item, you will get a notification indicating y
|
||||
|
||||
## FAQs
|
||||
- Do I need the DLC to play this?
|
||||
- Yes, you need both Rise & Fall and Gathering Storm.
|
||||
- You need both expansions, Rise & Fall and Gathering Storm. You do not need the other DLCs but they fully work with this.
|
||||
- Does this work with Multiplayer?
|
||||
- It does not and, despite my best efforts, probably won't until there's a new way for external programs to be able to interact with the game.
|
||||
- Does my mod that reskins Barbarians as various Pro Wrestlers work with this?
|
||||
- Only one way to find out! Any mods that modify techs/civics will most likely cause issues, though.
|
||||
- Does this work with other mods?
|
||||
- A lot of mods seem to work without issues combined with this, but you should avoid any mods that change things in the tech or civic tree, as even if they would work it could cause issues with the logic.
|
||||
- "Help! I can't see any of the items that have been sent to me!"
|
||||
- Both trees by default will show you the researchable Archipelago locations. To view the normal tree, you can click "Toggle Archipelago Tree" in the top-left corner of the tree view.
|
||||
- "Oh no! I received the Machinery tech and now instead of getting an Archer next turn, I have to wait an additional 10 turns to get a Crossbowman!"
|
||||
- Vanilla prevents you from building units of the same class from an earlier tech level after you have researched a later variant. For example, this could be problematic if someone unlocks Crossbowmen for you right out the gate since you won't be able to make Archers (which have a much lower production cost).
|
||||
Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
|
||||
- Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
|
||||
- If you think you should be able to make Field Cannons but seemingly can't try disabling `Telecommunications`
|
||||
- "How does DeathLink work? Am I going to have to start a new game every time one of my friends dies?"
|
||||
- Heavens no, my fellow Archipelago appreciator. When configuring your Archipelago options for Civilization on the options page, there are several choices available for you to fine tune the way you'd like to be punished for the follies of your friends. These include: Having a random unit destroyed, losing a percentage of gold or faith, or even losing a point on your era score. If you can't make up your mind, you can elect to have any of them be selected every time a death link is sent your way.
|
||||
In the event you lose one of your units in combat (this means captured units don't count), then you will send a death link event to the rest of your friends.
|
||||
@@ -39,7 +40,8 @@ Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to
|
||||
1. `TECH_WRITING`
|
||||
2. `TECH_EDUCATION`
|
||||
3. `TECH_CHEMISTRY`
|
||||
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.json).
|
||||
- An important thing to note is that the seaport is part of progressive industrial zones, due to electricity having both an industrial zone building and the seaport.
|
||||
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.py).
|
||||
|
||||
## Boostsanity
|
||||
Boostsanity takes all of the Eureka & Inspiration events and makes them location checks. This feature is the one to change up the way Civilization is played in an AP multiworld/randomizer. What normally are mundane tasks that are passively collected now become a novel and interesting bucket list that you need to pay attention to in order to unlock items for yourself and others!
|
||||
@@ -56,4 +58,3 @@ Boosts have logic associated with them in order to verify you can always reach t
|
||||
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
|
||||
- There's too many boosts, how will I know which one's I should focus on?!
|
||||
- In order to give a little more focus to all the boosts rather than just arbitrarily picking them at random, items in both of the vanilla trees will now have an advisor icon on them if its associated boost contains a progression item.
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ This guide is meant to help you get up and running with Civilization VI in Archi
|
||||
|
||||
The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux)
|
||||
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux).
|
||||
|
||||
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) v0.4.5 or higher.
|
||||
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
- The latest version of the [Civ VI AP Mod](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
@@ -20,27 +22,32 @@ In the main menu, navigate to the "Game Options" page. On the "Game" menu, make
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
|
||||
|
||||
5. Your finished mod folder should look something like this:
|
||||
|
||||
- Civ VI Mods Directory
|
||||
- civilization_archipelago_mod
|
||||
- NewItems.xml
|
||||
- InitOptions.lua
|
||||
- Archipelago.modinfo
|
||||
- All the other mod files, etc.
|
||||
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
|
||||
|
||||
## Configuring your game
|
||||
|
||||
When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
|
||||
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you have troubles with file extension related stuff, make sure Windows shows file extensions as they are turned off by default. If you don't know how to turn them on it is just a quick google search away.
|
||||
|
||||
- If you are getting an error: "The remote computer refused the network connection", or something else related to the client (or tuner) not being able to connect, it likely indicates the tuner is not actually enabled. One simple way to verify that it is enabled is, after completing the setup steps, go to Main Menu → Options → Look for an option named "Tuner" and verify it is set to "Enabled"
|
||||
|
||||
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are.
|
||||
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are. This can resend certain items to you, like one time bonuses.
|
||||
|
||||
- If the archipelago mod does not appear in the mod selector in the game, make sure the mod is correctly placed as a folder in the `Sid Meier's Civilization VI\Mods` folder, there should not be any loose files in there only folders. As in the path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
|
||||
|
||||
- If it still does not appear make sure you have the right folder, one way to verify you are in the right place is to find the general folder area where your Civ VI save files are located.
|
||||
|
||||
- If you get an error when trying to start a game saying `Error - One or more Mods failed to load content`, make sure the files from the `.apcivvi` are placed into the `civilization_archipelago_mod` as loose files and not as a folder.
|
||||
|
||||
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
|
||||
|
||||
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.
|
||||
|
||||
@@ -734,8 +734,8 @@ def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bo
|
||||
magic_items_array[array_offset] += 1
|
||||
|
||||
# Add the start inventory arrays to the offset data in bytes form.
|
||||
start_inventory_data[0x680080] = bytes(magic_items_array)
|
||||
start_inventory_data[0x6800A0] = bytes(cards_array)
|
||||
start_inventory_data[0x690080] = bytes(magic_items_array)
|
||||
start_inventory_data[0x6900A0] = bytes(cards_array)
|
||||
|
||||
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
|
||||
# possible Max Ups.
|
||||
|
||||
@@ -132,40 +132,40 @@ start_inventory_giver = [
|
||||
|
||||
# Magic Items
|
||||
0x13, 0x48, # ldr r0, =0x202572F
|
||||
0x14, 0x49, # ldr r1, =0x8680080
|
||||
0x14, 0x49, # ldr r1, =0x8690080
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x08, 0x2A, # cmp r2, #8
|
||||
0xFA, 0xDB, # blt 0x8680006
|
||||
0xFA, 0xDB, # blt 0x8690006
|
||||
# Max Ups
|
||||
0x11, 0x48, # ldr r0, =0x202572C
|
||||
0x12, 0x49, # ldr r1, =0x8680090
|
||||
0x12, 0x49, # ldr r1, =0x8690090
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x03, 0x2A, # cmp r2, #3
|
||||
0xFA, 0xDB, # blt 0x8680016
|
||||
0xFA, 0xDB, # blt 0x8690016
|
||||
# Cards
|
||||
0x0F, 0x48, # ldr r0, =0x2025674
|
||||
0x10, 0x49, # ldr r1, =0x86800A0
|
||||
0x10, 0x49, # ldr r1, =0x86900A0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x14, 0x2A, # cmp r2, #0x14
|
||||
0xFA, 0xDB, # blt 0x8680026
|
||||
0xFA, 0xDB, # blt 0x8690026
|
||||
# Inventory Items (not currently supported)
|
||||
0x0D, 0x48, # ldr r0, =0x20256ED
|
||||
0x0E, 0x49, # ldr r1, =0x86800C0
|
||||
0x0E, 0x49, # ldr r1, =0x86900C0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x36, 0x2A, # cmp r2, #36
|
||||
0xFA, 0xDB, # blt 0x8680036
|
||||
0xFA, 0xDB, # blt 0x8690036
|
||||
# Return to the function that checks for Magician Mode.
|
||||
0xBA, 0x21, # movs r1, #0xBA
|
||||
0x89, 0x00, # lsls r1, r1, #2
|
||||
@@ -176,13 +176,13 @@ start_inventory_giver = [
|
||||
# LDR number pool
|
||||
0x78, 0x7F, 0x00, 0x08,
|
||||
0x2F, 0x57, 0x02, 0x02,
|
||||
0x80, 0x00, 0x68, 0x08,
|
||||
0x80, 0x00, 0x69, 0x08,
|
||||
0x2C, 0x57, 0x02, 0x02,
|
||||
0x90, 0x00, 0x68, 0x08,
|
||||
0x90, 0x00, 0x69, 0x08,
|
||||
0x74, 0x56, 0x02, 0x02,
|
||||
0xA0, 0x00, 0x68, 0x08,
|
||||
0xA0, 0x00, 0x69, 0x08,
|
||||
0xED, 0x56, 0x02, 0x02,
|
||||
0xC0, 0x00, 0x68, 0x08,
|
||||
0xC0, 0x00, 0x69, 0x08,
|
||||
]
|
||||
|
||||
max_max_up_checker = [
|
||||
|
||||
@@ -335,8 +335,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
||||
rom_data.write_bytes(0x679A60, patches.kickless_roc_height_shortener)
|
||||
|
||||
# Give the player their Start Inventory upon entering their name on a new file.
|
||||
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x68, 0x08])
|
||||
rom_data.write_bytes(0x680000, patches.start_inventory_giver)
|
||||
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x69, 0x08])
|
||||
rom_data.write_bytes(0x690000, patches.start_inventory_giver)
|
||||
|
||||
# Prevent Max Ups from exceeding 255.
|
||||
rom_data.write_bytes(0x5E170, [0x00, 0x4A, 0x97, 0x46, 0x00, 0x00, 0x6A, 0x08])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
|
||||
self.growthlevel = None
|
||||
self.kh2connected = False
|
||||
self.kh2_finished_game = False
|
||||
self.serverconneced = False
|
||||
self.serverconnected = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
self.kh2_data_package = {}
|
||||
@@ -47,6 +47,8 @@ class KH2Context(CommonContext):
|
||||
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
||||
|
||||
self.sending = []
|
||||
self.slot_name = None
|
||||
self.disconnect_from_server = False
|
||||
# list used to keep track of locations+items player has. Used for disoneccting
|
||||
self.kh2_seed_save_cache = {
|
||||
"itemIndex": -1,
|
||||
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
|
||||
if password_requested and not self.password:
|
||||
await super(KH2Context, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
# if slot name != first time login or previous name
|
||||
# and seed name is none or saved seed name
|
||||
if not self.slot_name and not self.kh2seedname:
|
||||
await self.send_connect()
|
||||
elif self.slot_name == self.auth and self.kh2seedname:
|
||||
await self.send_connect()
|
||||
else:
|
||||
logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}")
|
||||
self.serverconnected = False
|
||||
self.disconnect_from_server = True
|
||||
|
||||
async def connection_closed(self):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.serverconnected = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
@@ -197,7 +208,8 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.serverconnected = False
|
||||
self.locations_checked = []
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
@@ -239,7 +251,15 @@ class KH2Context(CommonContext):
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "RoomInfo":
|
||||
self.kh2seedname = args['seed_name']
|
||||
if not self.kh2seedname:
|
||||
self.kh2seedname = args['seed_name']
|
||||
elif self.kh2seedname != args['seed_name']:
|
||||
self.disconnect_from_server = True
|
||||
self.serverconnected = False
|
||||
self.kh2connected = False
|
||||
logger.info("Connection to the wrong seed, connect to the correct seed or close the client.")
|
||||
return
|
||||
|
||||
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
|
||||
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
|
||||
|
||||
@@ -338,7 +358,7 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
},
|
||||
}
|
||||
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced:
|
||||
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected:
|
||||
self.kh2_seed_save_cache["itemIndex"] = start_index
|
||||
for item in args['items']:
|
||||
asyncio.create_task(self.give_item(item.item, item.location))
|
||||
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
|
||||
#
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
logger.info("Game is not open.")
|
||||
self.serverconneced = True
|
||||
|
||||
self.serverconnected = True
|
||||
self.slot_name = self.auth
|
||||
|
||||
def data_package_kh2_cache(self, loc_to_id, item_to_id):
|
||||
self.kh2_loc_name_to_id = loc_to_id
|
||||
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def give_item(self, item, location):
|
||||
try:
|
||||
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
|
||||
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
|
||||
# sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
|
||||
while not self.lookup_id_to_item:
|
||||
await asyncio.sleep(0.5)
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemdata = self.item_name_to_data[itemname]
|
||||
# itemcode = self.kh2_item_name_to_id[itemname]
|
||||
if itemdata.ability:
|
||||
if location in self.all_weapon_location_id:
|
||||
return
|
||||
# growth have reserved ability slots because of how the goa handles them
|
||||
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
|
||||
return
|
||||
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
|
||||
# appending the slot that the ability should be in
|
||||
# appending the slot that the ability should be in
|
||||
# abilities have a limit amount of slots.
|
||||
# we start from the back going down to not mess with stuff.
|
||||
# Front of Invo
|
||||
# Sora: Save+24F0+0x54 : 0x2546
|
||||
# Donald: Save+2604+0x54 : 0x2658
|
||||
# Goofy: Save+2718+0x54 : 0x276C
|
||||
# Back of Invo. Sora has 6 ability slots that are reserved
|
||||
# Sora: Save+24F0+0x54+0x92 : 0x25D8
|
||||
# Donald: Save+2604+0x54+0x9C : 0x26F4
|
||||
# Goofy: Save+2718+0x54+0x9C : 0x2808
|
||||
# seed has 2 scans in sora's abilities
|
||||
# recieved second scan
|
||||
# if len(seed_save(Scan:[ability slot 52]) < (2)amount of that ability they should have from slot data
|
||||
# ability_slot = back of inventory that isnt taken
|
||||
# add ability_slot to seed_save(Scan[]) so now its Scan:[ability slot 52,50]
|
||||
# decrease back of inventory since its ability_slot is already taken
|
||||
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
if itemname in self.sora_ability_set:
|
||||
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
|
||||
if ability_slot in self.front_ability_slots:
|
||||
self.front_ability_slots.remove(ability_slot)
|
||||
|
||||
# if itemdata in {bitmask} all the forms,summons and a few other things are bitmasks
|
||||
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
|
||||
# if memaddr is in a bitmask location in memory
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
|
||||
|
||||
# if itemdata in {magic}
|
||||
elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
||||
# if memaddr is in magic addresses
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
|
||||
|
||||
# equipment is a list instead of dict because you can only have 1 currently
|
||||
elif itemname in self.all_equipment:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
|
||||
|
||||
# weapons are done differently since you can only have one and has to check it differently
|
||||
elif itemname in self.all_weapons:
|
||||
if itemname in self.keyblade_set:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
|
||||
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
|
||||
else:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
|
||||
|
||||
# TODO: this can just be removed and put into the else below it
|
||||
elif itemname in self.stat_increase_set:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
|
||||
else:
|
||||
# "normal" items. They have a unique byte reserved for how many they have
|
||||
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
|
||||
else:
|
||||
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
|
||||
async def kh2_watcher(ctx: KH2Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
if ctx.kh2connected and ctx.serverconneced:
|
||||
if ctx.kh2connected and ctx.serverconnected:
|
||||
ctx.sending = []
|
||||
await asyncio.create_task(ctx.checkWorldLocations())
|
||||
await asyncio.create_task(ctx.checkLevels())
|
||||
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
if ctx.sending:
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
|
||||
elif not ctx.kh2connected and ctx.serverconnected:
|
||||
logger.info("Game Connection lost. trying to reconnect.")
|
||||
ctx.kh2 = None
|
||||
while not ctx.kh2connected and ctx.serverconneced:
|
||||
await asyncio.sleep(15)
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
ctx.get_addresses()
|
||||
while not ctx.kh2connected and ctx.serverconnected:
|
||||
try:
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
ctx.get_addresses()
|
||||
logger.info("Game Connection Established.")
|
||||
except Exception as e:
|
||||
await asyncio.sleep(5)
|
||||
if ctx.disconnect_from_server:
|
||||
ctx.disconnect_from_server = False
|
||||
await ctx.disconnect()
|
||||
except Exception as e:
|
||||
if ctx.kh2connected:
|
||||
ctx.kh2connected = False
|
||||
|
||||
@@ -277,9 +277,7 @@ class KH2World(World):
|
||||
if self.options.FillerItemsLocal:
|
||||
for item in filler_items:
|
||||
self.options.local_items.value.add(item)
|
||||
# By imitating remote this doesn't have to be plandoded filler anymore
|
||||
# for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
|
||||
# self.plando_locations[location] = random_stt_item
|
||||
|
||||
if not self.options.SummonLevelLocationToggle:
|
||||
self.total_locations -= 6
|
||||
|
||||
@@ -400,6 +398,8 @@ class KH2World(World):
|
||||
# plando goofy get bonuses
|
||||
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
||||
Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
|
||||
if len(goofy_get_bonus_location_pool) > len(self.goofy_get_bonus_abilities):
|
||||
raise Exception(f"Too little abilities to fill goofy get bonus locations for player {self.player_name}.")
|
||||
for location in goofy_get_bonus_location_pool:
|
||||
self.random.choice(self.goofy_get_bonus_abilities)
|
||||
random_ability = self.random.choice(self.goofy_get_bonus_abilities)
|
||||
@@ -416,11 +416,12 @@ class KH2World(World):
|
||||
random_ability = self.random.choice(self.donald_weapon_abilities)
|
||||
location.place_locked_item(random_ability)
|
||||
self.donald_weapon_abilities.remove(random_ability)
|
||||
|
||||
# if option is turned off
|
||||
if not self.options.DonaldGoofyStatsanity:
|
||||
# plando goofy get bonuses
|
||||
donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
|
||||
Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
|
||||
if len(donald_get_bonus_location_pool) > len(self.donald_get_bonus_abilities):
|
||||
raise Exception(f"Too little abilities to fill donald get bonus locations for player {self.player_name}.")
|
||||
for location in donald_get_bonus_location_pool:
|
||||
random_ability = self.random.choice(self.donald_get_bonus_abilities)
|
||||
location.place_locked_item(random_ability)
|
||||
|
||||
@@ -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.
@@ -23,6 +23,7 @@ DATA_LOCATIONS = {
|
||||
"DexSanityFlag": (0x1A71, 19),
|
||||
"GameStatus": (0x1A84, 0x01),
|
||||
"Money": (0x141F, 3),
|
||||
"CurrentMap": (0x1436, 1),
|
||||
"ResetCheck": (0x0100, 4),
|
||||
# First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
|
||||
# First should never be above 0x0F. This is just before Event Flags.
|
||||
@@ -65,6 +66,7 @@ class PokemonRBClient(BizHawkClient):
|
||||
self.banking_command = None
|
||||
self.game_state = False
|
||||
self.last_death_link = 0
|
||||
self.current_map = 0
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
|
||||
@@ -230,6 +232,10 @@ class PokemonRBClient(BizHawkClient):
|
||||
}])
|
||||
self.banking_command = None
|
||||
|
||||
if data["CurrentMap"][0] != self.current_map:
|
||||
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
|
||||
self.current_map = data["CurrentMap"][0]
|
||||
|
||||
# VICTORY
|
||||
|
||||
if data["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
|
||||
@@ -15,7 +15,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
|
||||
## Configuring BizHawk
|
||||
@@ -109,7 +109,7 @@ server uses password, type in the bottom textfield `/connect <address>:<port> [p
|
||||
|
||||
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
|
||||
|
||||
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Open PopTracker, and load the Pokémon Red and Blue pack.
|
||||
3. Click on the "AP" symbol at the top.
|
||||
4. Enter the AP address, slot name and password.
|
||||
|
||||
@@ -16,7 +16,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
|
||||
|
||||
## Software Opcional
|
||||
|
||||
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
|
||||
## Configurando BizHawk
|
||||
@@ -114,7 +114,7 @@ presiona enter (si el servidor usa contraseña, escribe en el campo de texto inf
|
||||
|
||||
Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
|
||||
|
||||
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
|
||||
3. Haz clic en el símbolo "AP" en la parte superior.
|
||||
4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -386,7 +386,7 @@ coppper_slot_machine = skill_recipe(ModMachine.copper_slot_machine, ModSkill.luc
|
||||
Forageable.salmonberry: 1, Material.clay: 1, Trash.joja_cola: 1}, ModNames.luck_skill)
|
||||
|
||||
gold_slot_machine = skill_recipe(ModMachine.gold_slot_machine, ModSkill.luck, 4, {MetalBar.gold: 15, ModMachine.copper_slot_machine: 1}, ModNames.luck_skill)
|
||||
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 4, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
|
||||
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 4, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
|
||||
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 6, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
|
||||
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 8, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
|
||||
|
||||
all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes}
|
||||
|
||||
@@ -6,7 +6,7 @@ id,name,classification,groups,mod_name
|
||||
18,Greenhouse,progression,COMMUNITY_REWARD,
|
||||
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
|
||||
20,Minecarts Repair,useful,COMMUNITY_REWARD,
|
||||
21,Bus Repair,progression,COMMUNITY_REWARD,
|
||||
21,Bus Repair,progression,"COMMUNITY_REWARD,DESERT_TRANSPORTATION",
|
||||
22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD,
|
||||
23,Stardrop,progression,,
|
||||
24,Progressive Backpack,progression,,
|
||||
@@ -63,8 +63,8 @@ id,name,classification,groups,mod_name
|
||||
77,Combat Level,progression,SKILL_LEVEL_UP,
|
||||
78,Earth Obelisk,progression,WIZARD_BUILDING,
|
||||
79,Water Obelisk,progression,WIZARD_BUILDING,
|
||||
80,Desert Obelisk,progression,WIZARD_BUILDING,
|
||||
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND",
|
||||
80,Desert Obelisk,progression,"WIZARD_BUILDING,DESERT_TRANSPORTATION",
|
||||
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND,ISLAND_TRANSPORTATION",
|
||||
82,Junimo Hut,useful,WIZARD_BUILDING,
|
||||
83,Gold Clock,progression,WIZARD_BUILDING,
|
||||
84,Progressive Coop,progression,BUILDING,
|
||||
@@ -242,7 +242,7 @@ id,name,classification,groups,mod_name
|
||||
257,Peach Sapling,progression,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
|
||||
258,Banana Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
|
||||
259,Mango Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
|
||||
260,Boat Repair,progression,GINGER_ISLAND,
|
||||
260,Boat Repair,progression,"GINGER_ISLAND,ISLAND_TRANSPORTATION",
|
||||
261,Open Professor Snail Cave,progression,GINGER_ISLAND,
|
||||
262,Island North Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
263,Island West Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
|
||||
|
||||
|
@@ -33,6 +33,8 @@ class Group(enum.Enum):
|
||||
SKILL_MASTERY = enum.auto()
|
||||
BUILDING = enum.auto()
|
||||
WIZARD_BUILDING = enum.auto()
|
||||
DESERT_TRANSPORTATION = enum.auto()
|
||||
ISLAND_TRANSPORTATION = enum.auto()
|
||||
ARCADE_MACHINE_BUFFS = enum.auto()
|
||||
BASE_RESOURCE = enum.auto()
|
||||
WARP_TOTEM = enum.auto()
|
||||
|
||||
@@ -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}"):
|
||||
|
||||
@@ -1,61 +1,122 @@
|
||||
from typing import Any
|
||||
|
||||
tww_options_presets: dict[str, dict[str, Any]] = {
|
||||
"Tournament S7": {
|
||||
"Tournament S8": {
|
||||
"progression_dungeon_secrets": True,
|
||||
"progression_combat_secret_caves": True,
|
||||
"progression_short_sidequests": True,
|
||||
"progression_long_sidequests": True,
|
||||
"progression_spoils_trading": True,
|
||||
"progression_big_octos_gunboats": True,
|
||||
"progression_mail": True,
|
||||
"progression_platforms_rafts": True,
|
||||
"progression_submarines": True,
|
||||
"progression_big_octos_gunboats": True,
|
||||
"progression_expensive_purchases": True,
|
||||
"progression_island_puzzles": True,
|
||||
"progression_misc": True,
|
||||
"randomize_mapcompass": "startwith",
|
||||
"randomize_bigkeys": "startwith",
|
||||
"required_bosses": True,
|
||||
"num_required_bosses": 3,
|
||||
"num_required_bosses": 4,
|
||||
"included_dungeons": ["Forsaken Fortress"],
|
||||
"chest_type_matches_contents": True,
|
||||
"logic_obscurity": "hard",
|
||||
"randomize_dungeon_entrances": True,
|
||||
"randomize_starting_island": True,
|
||||
"add_shortcut_warps_between_dungeons": True,
|
||||
"start_inventory_from_pool": {
|
||||
"Telescope": 1,
|
||||
"Wind Waker": 1,
|
||||
"Goddess Tingle Statue": 1,
|
||||
"Earth Tingle Statue": 1,
|
||||
"Wind Tingle Statue": 1,
|
||||
"Wind's Requiem": 1,
|
||||
"Ballad of Gales": 1,
|
||||
"Command Melody": 1,
|
||||
"Earth God's Lyric": 1,
|
||||
"Wind God's Aria": 1,
|
||||
"Song of Passing": 1,
|
||||
"Progressive Magic Meter": 2,
|
||||
"Triforce Shard 1": 1,
|
||||
"Triforce Shard 2": 1,
|
||||
"Triforce Shard 3": 1,
|
||||
"Skull Necklace": 20,
|
||||
"Golden Feather": 20,
|
||||
"Knight's Crest": 10,
|
||||
"Green Chu Jelly": 15,
|
||||
"Nayru's Pearl": 1,
|
||||
"Din's Pearl": 1,
|
||||
},
|
||||
"start_location_hints": ["Ganon's Tower - Maze Chest"],
|
||||
"start_location_hints": [
|
||||
"Windfall Island - Chu Jelly Juice Shop - Give 15 Blue Chu Jelly",
|
||||
"Ganon's Tower - Maze Chest",
|
||||
],
|
||||
"exclude_locations": [
|
||||
"Outset Island - Orca - Give 10 Knight's Crests",
|
||||
"Outset Island - Great Fairy",
|
||||
"Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly",
|
||||
"Windfall Island - Mrs. Marie - Give 1 Joy Pendant",
|
||||
"Windfall Island - Mrs. Marie - Give 21 Joy Pendants",
|
||||
"Windfall Island - Mrs. Marie - Give 40 Joy Pendants",
|
||||
"Windfall Island - Maggie's Father - Give 20 Skull Necklaces",
|
||||
"Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers",
|
||||
"Windfall Island - Lenzo's House - Become Lenzo's Assistant",
|
||||
"Windfall Island - Lenzo's House - Bring Forest Firefly",
|
||||
"Windfall Island - Sam - Decorate the Town",
|
||||
"Windfall Island - Kamo - Full Moon Photo",
|
||||
"Windfall Island - Linda and Anton",
|
||||
"Dragon Roost Island - Secret Cave",
|
||||
"Greatfish Isle - Hidden Chest",
|
||||
"Mother and Child Isles - Inside Mother Isle",
|
||||
"Fire Mountain - Cave - Chest",
|
||||
"Fire Mountain - Lookout Platform Chest",
|
||||
"Fire Mountain - Lookout Platform - Destroy the Cannons",
|
||||
"Fire Mountain - Big Octo",
|
||||
"Mailbox - Letter from Hoskit's Girlfriend",
|
||||
"Headstone Island - Top of the Island",
|
||||
"Headstone Island - Submarine",
|
||||
"Earth Temple - Behind Curtain Next to Hammer Button",
|
||||
"The Great Sea - Goron Trading Reward",
|
||||
"The Great Sea - Withered Trees",
|
||||
"Private Oasis - Big Octo",
|
||||
"Boating Course - Raft",
|
||||
"Boating Course - Cave",
|
||||
"Stone Watcher Island - Cave",
|
||||
"Stone Watcher Island - Lookout Platform Chest",
|
||||
"Stone Watcher Island - Lookout Platform - Destroy the Cannons",
|
||||
"Overlook Island - Cave",
|
||||
"Bird's Peak Rock - Cave",
|
||||
"Pawprint Isle - Wizzrobe Cave",
|
||||
"Thorned Fairy Island - Great Fairy",
|
||||
"Thorned Fairy Island - Northeastern Lookout Platform - Destroy the Cannons",
|
||||
"Thorned Fairy Island - Southwestern Lookout Platform - Defeat the Enemies",
|
||||
"Eastern Fairy Island - Great Fairy",
|
||||
"Eastern Fairy Island - Lookout Platform - Defeat the Cannons and Enemies",
|
||||
"Western Fairy Island - Great Fairy",
|
||||
"Southern Fairy Island - Great Fairy",
|
||||
"Northern Fairy Island - Great Fairy",
|
||||
"Western Fairy Island - Lookout Platform",
|
||||
"Tingle Island - Ankle - Reward for All Tingle Statues",
|
||||
"Tingle Island - Big Octo",
|
||||
"Diamond Steppe Island - Big Octo",
|
||||
"Rock Spire Isle - Cave",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item",
|
||||
"Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item",
|
||||
"Rock Spire Isle - Western Lookout Platform - Destroy the Cannons",
|
||||
"Rock Spire Isle - Eastern Lookout Platform - Destroy the Cannons",
|
||||
"Rock Spire Isle - Center Lookout Platform",
|
||||
"Rock Spire Isle - Southeast Gunboat",
|
||||
"Shark Island - Cave",
|
||||
"Horseshoe Island - Northwestern Lookout Platform",
|
||||
"Horseshoe Island - Southeastern Lookout Platform",
|
||||
"Flight Control Platform - Submarine",
|
||||
"Star Island - Cave",
|
||||
"Star Island - Lookout Platform",
|
||||
"Star Belt Archipelago - Lookout Platform",
|
||||
"Five-Star Isles - Lookout Platform - Destroy the Cannons",
|
||||
"Five-Star Isles - Raft",
|
||||
"Five-Star Isles - Submarine",
|
||||
"Seven-Star Isles - Center Lookout Platform",
|
||||
"Seven-Star Isles - Northern Lookout Platform",
|
||||
"Seven-Star Isles - Southern Lookout Platform",
|
||||
"Seven-Star Isles - Big Octo",
|
||||
"Cyclops Reef - Lookout Platform - Defeat the Enemies",
|
||||
"Two-Eye Reef - Lookout Platform",
|
||||
"Two-Eye Reef - Big Octo Great Fairy",
|
||||
"Five-Eye Reef - Lookout Platform",
|
||||
"Six-Eye Reef - Lookout Platform - Destroy the Cannons",
|
||||
"Six-Eye Reef - Submarine",
|
||||
],
|
||||
},
|
||||
"Miniblins 2025": {
|
||||
|
||||
@@ -76,10 +76,11 @@ at least normal.
|
||||
|
||||
A few presets are available on the [player options page](../player-options) for your convenience.
|
||||
|
||||
- **Tournament S7**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[Season 7 Tournament](https://docs.google.com/document/d/1mJj7an-DvpYilwNt-DdlFOy1fz5_NMZaPZvHeIekplc).
|
||||
The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options
|
||||
may seem intimidating, the preset also excludes several locations.
|
||||
- **Tournament S8**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[Season 8 Tournament](https://docs.google.com/document/d/1b8F5DL3P5fgsQC_URiwhpMfqTpsGh2M-KmtTdXVigh4).
|
||||
The preset features 4 required bosses (with Helmaroc King guaranteed required), dungeon entrance rando, hard obscurity
|
||||
difficulty, and a variety of overworld checks. While the list of enabled progression options may seem intimidating,
|
||||
the preset also excludes several locations and starts you with a handful of items.
|
||||
- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[2025 Season of Miniblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
|
||||
preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to
|
||||
|
||||
@@ -110,6 +110,14 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
|
||||
else:
|
||||
filler_pool.extend([item] * data.quantity)
|
||||
|
||||
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
|
||||
if world.options.sword_mode == "start_with_sword":
|
||||
precollected_items.append("Progressive Sword")
|
||||
progression_pool.remove("Progressive Sword")
|
||||
# Or, if it's swordless mode, remove all swords from the item pool.
|
||||
elif world.options.sword_mode == "swordless":
|
||||
useful_pool = [item for item in useful_pool if item != "Progressive Sword"]
|
||||
|
||||
# Assign useful and filler items to item pools in the world.
|
||||
world.random.shuffle(useful_pool)
|
||||
world.random.shuffle(filler_pool)
|
||||
@@ -141,17 +149,6 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
|
||||
pool.extend(progression_pool)
|
||||
num_items_left_to_place -= len(progression_pool)
|
||||
|
||||
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
|
||||
if world.options.sword_mode == "start_with_sword":
|
||||
precollected_items.append("Progressive Sword")
|
||||
num_items_left_to_place += 1
|
||||
pool.remove("Progressive Sword")
|
||||
# Or, if it's swordless mode, remove all swords from the item pool.
|
||||
elif world.options.sword_mode == "swordless":
|
||||
while "Progressive Sword" in pool:
|
||||
num_items_left_to_place += 1
|
||||
pool.remove("Progressive Sword")
|
||||
|
||||
# Place useful items, then filler items to fill out the remaining locations.
|
||||
pool.extend([world.get_filler_item_name(strict=False) for _ in range(num_items_left_to_place)])
|
||||
|
||||
|
||||
@@ -496,70 +496,74 @@ class WargrooveContext(CommonContext):
|
||||
|
||||
async def game_watcher(ctx: WargrooveContext):
|
||||
while not ctx.exit_event.is_set():
|
||||
if ctx.syncing == True:
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
ctx.syncing = False
|
||||
sending = []
|
||||
victory = False
|
||||
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||
for file in files:
|
||||
if file == "deathLinkSend" and ctx.has_death_link:
|
||||
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
|
||||
failed_mission = f.read()
|
||||
if ctx.slot is not None:
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
sending = sending+[(int(st))]
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file.find("victory") > -1:
|
||||
victory = True
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file == "unitSacrifice" or file == "unitSacrificeAI":
|
||||
if ctx.has_sacrifice_summon:
|
||||
stored_units_key = ctx.player_stored_units_key
|
||||
if file == "unitSacrificeAI":
|
||||
stored_units_key = ctx.ai_stored_units_key
|
||||
try:
|
||||
if ctx.syncing == True:
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
ctx.syncing = False
|
||||
sending = []
|
||||
victory = False
|
||||
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||
for file in files:
|
||||
if file == "deathLinkSend" and ctx.has_death_link:
|
||||
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
|
||||
unit_class = f.read()
|
||||
message = [{"cmd": 'Set', "key": stored_units_key,
|
||||
"default": [],
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
|
||||
await ctx.send_msgs(message)
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
|
||||
if ctx.has_sacrifice_summon:
|
||||
stored_units_key = ctx.player_stored_units_key
|
||||
if file == "unitSummonRequestAI":
|
||||
stored_units_key = ctx.ai_stored_units_key
|
||||
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
|
||||
if stored_units_key in ctx.stored_data:
|
||||
stored_units = ctx.stored_data[stored_units_key]
|
||||
if stored_units is None:
|
||||
stored_units = []
|
||||
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
|
||||
if len(wg1_stored_units) != 0:
|
||||
summoned_unit = random.choice(wg1_stored_units)
|
||||
message = [{"cmd": 'Set', "key": stored_units_key,
|
||||
"default": [],
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
|
||||
await ctx.send_msgs(message)
|
||||
f.write(summoned_unit)
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
failed_mission = f.read()
|
||||
if ctx.slot is not None:
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
sending = sending+[(int(st))]
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file.find("victory") > -1:
|
||||
victory = True
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file == "unitSacrifice" or file == "unitSacrificeAI":
|
||||
if ctx.has_sacrifice_summon:
|
||||
stored_units_key = ctx.player_stored_units_key
|
||||
if file == "unitSacrificeAI":
|
||||
stored_units_key = ctx.ai_stored_units_key
|
||||
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
|
||||
unit_class = f.read()
|
||||
message = [{"cmd": 'Set', "key": stored_units_key,
|
||||
"default": [],
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
|
||||
await ctx.send_msgs(message)
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
|
||||
if ctx.has_sacrifice_summon:
|
||||
stored_units_key = ctx.player_stored_units_key
|
||||
if file == "unitSummonRequestAI":
|
||||
stored_units_key = ctx.ai_stored_units_key
|
||||
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
|
||||
if stored_units_key in ctx.stored_data:
|
||||
stored_units = ctx.stored_data[stored_units_key]
|
||||
if stored_units is None:
|
||||
stored_units = []
|
||||
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
|
||||
if len(wg1_stored_units) != 0:
|
||||
summoned_unit = random.choice(wg1_stored_units)
|
||||
message = [{"cmd": 'Set', "key": stored_units_key,
|
||||
"default": [],
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
|
||||
await ctx.send_msgs(message)
|
||||
f.write(summoned_unit)
|
||||
os.remove(os.path.join(ctx.game_communication_path, file))
|
||||
|
||||
ctx.locations_checked = sending
|
||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
await asyncio.sleep(0.1)
|
||||
ctx.locations_checked = sending
|
||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as err:
|
||||
logger.warn("Exception in communication thread, a check may not have been sent: " + str(err))
|
||||
|
||||
|
||||
def print_error_and_close(msg):
|
||||
|
||||
Reference in New Issue
Block a user