mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-21 15:03:18 -07:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2666bacd7 | ||
|
|
4eefd9c3ce | ||
|
|
211456242e | ||
|
|
6f244c4661 | ||
|
|
47bf6d724b | ||
|
|
5c710ad032 | ||
|
|
dda5a05cbb | ||
|
|
e0a63e0290 | ||
|
|
9246659589 | ||
|
|
377cdb84b4 | ||
|
|
0e759f25fd | ||
|
|
b408bb4f6e | ||
|
|
1356479415 | ||
|
|
ec5b4e704f | ||
|
|
aa9e617510 | ||
|
|
ecb739ce96 | ||
|
|
3b72140435 | ||
|
|
27a6770569 | ||
|
|
2ff611167a | ||
|
|
e83e178b63 | ||
|
|
068a757373 | ||
|
|
0ad4527719 | ||
|
|
8c6327d024 | ||
|
|
aecbb2ab02 | ||
|
|
52b11083fe | ||
|
|
a8c87ce54b | ||
|
|
ddb3240591 | ||
|
|
f25ef639f2 | ||
|
|
ab7d3ce4aa | ||
|
|
50db922cef | ||
|
|
a2708edc37 | ||
|
|
603a5005e2 | ||
|
|
b4f68bce76 | ||
|
|
a76cec1539 | ||
|
|
694e6bcae3 | ||
|
|
b85b18cf5f | ||
|
|
04c707f874 | ||
|
|
99142fd662 | ||
|
|
0c5cb17d96 | ||
|
|
cabde313b5 | ||
|
|
8f68bb342d | ||
|
|
fab75d3a32 | ||
|
|
d19bf98dc4 | ||
|
|
b0f41c0360 | ||
|
|
6ebd60feaa | ||
|
|
dd6007b309 | ||
|
|
fde203379d | ||
|
|
fcb3efee01 | ||
|
|
19a21099ed | ||
|
|
20ca7e71c7 | ||
|
|
002202ff5f | ||
|
|
32487137e8 | ||
|
|
f327ab30a6 | ||
|
|
e7545cbc28 | ||
|
|
eba757d2cd | ||
|
|
4119763e23 | ||
|
|
e830a6d6f5 | ||
|
|
704cd97f21 | ||
|
|
47a0dd696f | ||
|
|
c64791e3a8 |
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)
|
||||
|
||||
39
Launcher.py
39
Launcher.py
@@ -11,6 +11,7 @@ Additional components can be added to worlds.LauncherComponents.components.
|
||||
import argparse
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -41,13 +42,17 @@ def open_host_yaml():
|
||||
if is_linux:
|
||||
exe = which('sensible-editor') or which('gedit') or \
|
||||
which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||
subprocess.Popen([exe, file])
|
||||
elif is_macos:
|
||||
exe = which("open")
|
||||
subprocess.Popen([exe, file])
|
||||
else:
|
||||
webbrowser.open(file)
|
||||
return
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
subprocess.Popen([exe, file], env=env)
|
||||
|
||||
def open_patch():
|
||||
suffixes = []
|
||||
@@ -92,7 +97,11 @@ def open_folder(folder_path):
|
||||
return
|
||||
|
||||
if exe:
|
||||
subprocess.Popen([exe, folder_path])
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
subprocess.Popen([exe, folder_path], env=env)
|
||||
else:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
|
||||
@@ -104,14 +113,21 @@ def update_settings():
|
||||
|
||||
components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml),
|
||||
Component("Open Patch", func=open_patch),
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("Open host.yaml", func=open_host_yaml,
|
||||
description="Open the host.yaml file to change settings for generation, games, and more."),
|
||||
Component("Open Patch", func=open_patch,
|
||||
description="Open a patch file, downloaded from the room page or provided by the host."),
|
||||
Component("Generate Template Options", func=generate_yamls,
|
||||
description="Generate template YAMLs for currently installed games."),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"),
|
||||
description="Open archipelago.gg in your browser."),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"),
|
||||
description="Join the Discord server to play public multiworlds, report issues, or just chat!"),
|
||||
Component("Unrated/18+ Discord Server", icon="discord",
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
|
||||
description="Find unrated and 18+ games in the After Dark Discord server."),
|
||||
Component("Browse Files", func=browse_files,
|
||||
description="Open the Archipelago installation folder in your file browser."),
|
||||
])
|
||||
|
||||
|
||||
@@ -180,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()
|
||||
@@ -1524,9 +1524,11 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
f"dictionary, not {type(items)}")
|
||||
locations = item.get("locations", [])
|
||||
if not locations:
|
||||
locations = item.get("location", ["Everywhere"])
|
||||
locations = item.get("location", [])
|
||||
if locations:
|
||||
count = 1
|
||||
else:
|
||||
locations = ["Everywhere"]
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
if not isinstance(locations, list):
|
||||
|
||||
@@ -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
|
||||
|
||||
42
Utils.py
42
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()
|
||||
@@ -226,7 +230,12 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||
from shutil import which
|
||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||
subprocess.call([open_command, filename])
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
subprocess.call([open_command, filename], env=env)
|
||||
|
||||
|
||||
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
|
||||
@@ -708,25 +717,30 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
|
||||
res.put(open_filename(*args))
|
||||
|
||||
|
||||
def _run_for_stdout(*args: str):
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
||||
|
||||
|
||||
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||
-> typing.Optional[str]:
|
||||
logging.info(f"Opening file input dialog for {title}.")
|
||||
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
|
||||
return _run_for_stdout(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||
selection = (f"--filename={suggest}",) if suggest else ()
|
||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
@@ -760,21 +774,18 @@ def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args
|
||||
|
||||
|
||||
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||
return _run_for_stdout(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||
os.path.abspath(suggest) if suggest else ".")
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = ("--directory",)
|
||||
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
@@ -801,9 +812,6 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
|
||||
|
||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_kivy_running():
|
||||
from kvui import MessageBox
|
||||
MessageBox(title, text, error).open()
|
||||
@@ -814,10 +822,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
||||
return _run_for_stdout(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
||||
return _run_for_stdout(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
||||
|
||||
elif is_windows:
|
||||
import ctypes
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
flask>=3.1.0
|
||||
flask>=3.1.1
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.2
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -119,9 +119,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# AP Container
|
||||
elif handler:
|
||||
data = zfile.open(file, "r").read()
|
||||
patch = handler(BytesIO(data))
|
||||
patch.read()
|
||||
files[patch.player] = data
|
||||
with zipfile.ZipFile(BytesIO(data)) as container:
|
||||
player = json.loads(container.open("archipelago.json").read())["player"]
|
||||
files[player] = data
|
||||
|
||||
# Spoiler
|
||||
elif file.filename.endswith(".txt"):
|
||||
@@ -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"
|
||||
|
||||
@@ -365,18 +365,14 @@ request_handlers = {
|
||||
["PREFERRED_CORES"] = function (req)
|
||||
local res = {}
|
||||
local preferred_cores = client.getconfig().PreferredCores
|
||||
local systems_enumerator = preferred_cores.Keys:GetEnumerator()
|
||||
|
||||
res["type"] = "PREFERRED_CORES_RESPONSE"
|
||||
res["value"] = {}
|
||||
res["value"]["NES"] = preferred_cores.NES
|
||||
res["value"]["SNES"] = preferred_cores.SNES
|
||||
res["value"]["GB"] = preferred_cores.GB
|
||||
res["value"]["GBC"] = preferred_cores.GBC
|
||||
res["value"]["DGB"] = preferred_cores.DGB
|
||||
res["value"]["SGB"] = preferred_cores.SGB
|
||||
res["value"]["PCE"] = preferred_cores.PCE
|
||||
res["value"]["PCECD"] = preferred_cores.PCECD
|
||||
res["value"]["SGX"] = preferred_cores.SGX
|
||||
|
||||
while systems_enumerator:MoveNext() do
|
||||
res["value"][systems_enumerator.Current] = preferred_cores[systems_enumerator.Current]
|
||||
end
|
||||
|
||||
return res
|
||||
end,
|
||||
|
||||
@@ -477,7 +477,7 @@ function main()
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
-- If we're uninitialized, attempt to make the connection.
|
||||
if (frame % 120 == 0) then
|
||||
server:settimeout(2)
|
||||
server:settimeout(120)
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
print('Initial Connection Made')
|
||||
|
||||
BIN
data/mcicon.ico
BIN
data/mcicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -121,9 +121,6 @@
|
||||
# The Messenger
|
||||
/worlds/messenger/ @alwaysintreble
|
||||
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
|
||||
@@ -117,12 +117,6 @@ flowchart LR
|
||||
%% Java Based Games
|
||||
subgraph Java
|
||||
JM[Mod with Archipelago.MultiClient.Java]
|
||||
subgraph Minecraft
|
||||
MCS[Minecraft Forge Server]
|
||||
JMC[Any Java Minecraft Clients]
|
||||
MCS <-- TCP --> JMC
|
||||
end
|
||||
JM <-- Forge Mod Loader --> MCS
|
||||
end
|
||||
AS <-- WebSockets --> JM
|
||||
|
||||
|
||||
@@ -258,31 +258,6 @@ another flag like "progression", it means "an especially useful progression item
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
||||
|
||||
### Events
|
||||
|
||||
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
||||
track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
|
||||
being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
|
||||
never made aware of them and these locations can never be checked, nor can the items be received during play.
|
||||
They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
|
||||
is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
|
||||
relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
|
||||
By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
|
||||
They must not exist in the `name_to_id` lookups, as they have no ID.
|
||||
|
||||
The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import ItemClassification
|
||||
from .subclasses import MyGameLocation, MyGameItem
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
victory_loc = MyGameLocation(self.player, "Victory", None)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
```
|
||||
|
||||
### Regions
|
||||
|
||||
Regions are logical containers that typically hold locations that share some common access rules. If location logic is
|
||||
@@ -339,6 +314,63 @@ avoiding the need for indirect conditions at the expense of performance.
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
reject the placement of an item there.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
Events are used to represent in-game actions (that aren't regular Archipelago locations) when either:
|
||||
|
||||
* We want to show in the spoiler log when the player is expected to perform the in-game action.
|
||||
* It's the cleanest way to represent how that in-game action impacts logic.
|
||||
|
||||
Typical examples include completing the goal, defeating a boss, or flipping a switch that affects multiple areas.
|
||||
|
||||
To be precise: the term "event" on its own refers to the special combination of an "event item" placed on an "event
|
||||
location". Event items and locations are created the same way as normal items and locations, except that they have an
|
||||
`id` of `None`, and an event item must be placed on an event location
|
||||
(and vice versa). Finally, although events are often described as "fake" items and locations, it's important to
|
||||
understand that they are perfectly real during generation.
|
||||
|
||||
The most common way to create an event is to create the event item and the event location, then immediately call
|
||||
`Location.place_locked_item()`:
|
||||
|
||||
```python
|
||||
victory_loc = MyGameLocation(self.player, "Defeat the Final Boss", None, final_boss_arena_region)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
set_rule(victory_loc, lambda state: state.has("Boss Defeating Sword", self.player))
|
||||
```
|
||||
|
||||
Requiring an event to finish the game will make the spoiler log display an additional
|
||||
`Defeat the Final Boss: Victory` line when the player is expected to finish, rather than only showing their last
|
||||
relevant item. But events aren't just about the spoiler log; a more substantial example of using events to structure
|
||||
your logic might be:
|
||||
|
||||
```python
|
||||
water_loc = MyGameLocation(self.player, "Water Level Switch", None, pump_station_region)
|
||||
water_loc.place_locked_item(MyGameItem("Lowered Water Level", ItemClassification.progression, None, self.player))
|
||||
pump_station_region.locations.append(water_loc)
|
||||
set_rule(water_loc, lambda state: state.has("Double Jump", self.player)) # the switch is really high up
|
||||
...
|
||||
basement_loc = MyGameLocation(self.player, "Flooded House - Basement Chest", None, flooded_house_region)
|
||||
flooded_house_region.locations += [upstairs_loc, ground_floor_loc, basement_loc]
|
||||
...
|
||||
set_rule(basement_loc, lambda state: state.has("Lowered Water Level", self.player))
|
||||
```
|
||||
|
||||
This creates a "Lowered Water Level" event and a regular location whose access rule depends on that
|
||||
event being reachable. If you made several more locations the same way, this would ensure all of those locations can
|
||||
only become reachable when the event location is reachable (i.e. when the water level can be lowered), without
|
||||
copy-pasting the event location's access rule and then repeatedly re-evaluating it. Also, the spoiler log will show
|
||||
`Water Level Switch: Lowered Water Level` when the player is expected to do this.
|
||||
|
||||
To be clear, this example could also be modeled with a second Region (perhaps "Un-Flooded House"). Or you could modify
|
||||
the game so flipping that switch checks a regular AP location in addition to lowering the water level.
|
||||
Events are never required, but it may be cleaner to use an event if e.g. flipping that switch affects the logic in
|
||||
dozens of half-flooded areas that would all otherwise need additional Regions, and you don't want it to be a regular
|
||||
location. It depends on the game.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Your World
|
||||
@@ -488,8 +520,8 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||
* `create_items(self)`
|
||||
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
||||
after this step. Locations cannot be moved to different regions after this step.
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions after
|
||||
this step. Locations cannot be moved to different regions after this step. This includes event items and locations.
|
||||
* `set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
* `connect_entrances(self)`
|
||||
|
||||
@@ -138,11 +138,6 @@ Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
176
kvui.py
176
kvui.py
@@ -60,7 +60,10 @@ from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupporting
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
|
||||
from kivymd.uix.screen import MDScreen
|
||||
from kivymd.uix.screenmanager import MDScreenManager
|
||||
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
@@ -726,6 +729,10 @@ class MessageBox(Popup):
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class MDNavigationItemBase(MDNavigationItem):
|
||||
text = StringProperty(None)
|
||||
|
||||
|
||||
class ButtonsPrompt(MDDialog):
|
||||
def __init__(self, title: str, text: str, response: typing.Callable[[str], None],
|
||||
*prompts: str, **kwargs) -> None:
|
||||
@@ -766,58 +773,34 @@ class ButtonsPrompt(MDDialog):
|
||||
)
|
||||
|
||||
|
||||
class ClientTabs(MDTabsSecondary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
class MDScreenManagerBase(MDScreenManager):
|
||||
current_tab: MDNavigationItemBase
|
||||
local_screen_names: list[str]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.local_screen_names = []
|
||||
|
||||
def _check_panel_height(self, *args):
|
||||
self.ids.tab_scroll.height = dp(38)
|
||||
|
||||
def update_indicator(
|
||||
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
||||
) -> None:
|
||||
def update_indicator(*args):
|
||||
indicator_pos = (0, 0)
|
||||
indicator_size = (0, 0)
|
||||
|
||||
item_text_object = self._get_tab_item_text_icon_object()
|
||||
|
||||
if item_text_object:
|
||||
indicator_pos = (
|
||||
instance.x + dp(12),
|
||||
self.indicator.pos[1]
|
||||
if not self._tabs_carousel
|
||||
else self._tabs_carousel.height,
|
||||
)
|
||||
indicator_size = (
|
||||
instance.width - dp(24),
|
||||
self.indicator_height,
|
||||
)
|
||||
|
||||
Animation(
|
||||
pos=indicator_pos,
|
||||
size=indicator_size,
|
||||
d=0 if not self.indicator_anim else self.indicator_duration,
|
||||
t=self.indicator_transition,
|
||||
).start(self.indicator)
|
||||
|
||||
if not instance:
|
||||
self.indicator.pos = (x, self.indicator.pos[1])
|
||||
self.indicator.size = (w, self.indicator_height)
|
||||
def add_widget(self, widget: Widget, *args, **kwargs) -> None:
|
||||
super().add_widget(widget, *args, **kwargs)
|
||||
if "index" in kwargs:
|
||||
self.local_screen_names.insert(kwargs["index"], widget.name)
|
||||
else:
|
||||
Clock.schedule_once(update_indicator)
|
||||
self.local_screen_names.append(widget.name)
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
self.ids.container.remove_widget(tab)
|
||||
self.carousel.remove_widget(content)
|
||||
self.on_size(self, self.size)
|
||||
def switch_screens(self, new_tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called whenever the user clicks a tab to switch to a different screen.
|
||||
|
||||
:param new_tab: The new screen to switch to's tab.
|
||||
"""
|
||||
name = new_tab.text
|
||||
if self.local_screen_names.index(name) > self.local_screen_names.index(self.current_screen.name):
|
||||
self.transition.direction = "left"
|
||||
else:
|
||||
self.transition.direction = "right"
|
||||
self.current = name
|
||||
self.current_tab = new_tab
|
||||
|
||||
|
||||
class CommandButton(MDButton, MDTooltip):
|
||||
@@ -845,6 +828,9 @@ class GameManager(ThemedApp):
|
||||
main_area_container: MDGridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
tabs: MDNavigationBar
|
||||
screens: MDScreenManagerBase
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
self.title = self.base_title
|
||||
self.ctx = ctx
|
||||
@@ -874,7 +860,7 @@ class GameManager(ThemedApp):
|
||||
@property
|
||||
def tab_count(self):
|
||||
if hasattr(self, "tabs"):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return max(1, len(self.tabs.children))
|
||||
return 1
|
||||
|
||||
def on_start(self):
|
||||
@@ -914,30 +900,30 @@ class GameManager(ThemedApp):
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||
self.screens = MDScreenManagerBase(pos_hint={"center_x": 0.5})
|
||||
self.tabs = MDNavigationBar(orientation="horizontal", size_hint_y=None, height=dp(40), set_bars_color=True)
|
||||
# bind the method to the bar for back compatibility
|
||||
self.tabs.remove_tab = self.remove_client_tab
|
||||
self.screens.current_tab = self.add_client_tab(
|
||||
"All" if len(self.logging_pairs) > 1 else "Archipelago",
|
||||
UILog(*(logging.getLogger(logger_name) for logger_name, name in self.logging_pairs)),
|
||||
)
|
||||
self.log_panels["All"] = self.screens.current_tab.content
|
||||
self.screens.current_tab.active = True
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
self.log_panels[display_name] = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||
panel.content = self.log_panels[display_name]
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.carousel.add_widget(panel.content)
|
||||
self.tabs.add_widget(panel)
|
||||
self.add_client_tab(display_name, self.log_panels[display_name])
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = HintLog(self.json_to_kivy_parser)
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout(self.hint_log))
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, cols=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
self.main_area_container.add_widget(self.screens)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
@@ -974,25 +960,61 @@ class GameManager(ThemedApp):
|
||||
|
||||
return self.container
|
||||
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> MDNavigationItemBase:
|
||||
"""
|
||||
Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content.
|
||||
|
||||
:param title: The title of the tab.
|
||||
:param content: The Widget to be added as content for this tab's new MDScreen. Will also be added to the
|
||||
returned tab as tab.content.
|
||||
:param index: The index to insert the tab at. Defaults to -1, meaning the tab will be appended to the end.
|
||||
|
||||
:return: The new tab.
|
||||
"""
|
||||
if self.tabs.children:
|
||||
self.tabs.add_widget(MDDivider(orientation="vertical"))
|
||||
new_tab = MDNavigationItemBase(text=title)
|
||||
new_tab.content = content
|
||||
if -1 < index <= len(self.tabs.carousel.slides):
|
||||
new_tab.bind(on_release=self.tabs.set_active_item)
|
||||
new_tab._tabs = self.tabs
|
||||
self.tabs.ids.container.add_widget(new_tab, index=index)
|
||||
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
||||
new_screen = MDScreen(name=title)
|
||||
new_screen.add_widget(content)
|
||||
if -1 < index <= len(self.tabs.children):
|
||||
remapped_index = len(self.tabs.children) - index
|
||||
self.tabs.add_widget(new_tab, index=remapped_index)
|
||||
self.screens.add_widget(new_screen, index=index)
|
||||
else:
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
self.screens.add_widget(new_screen)
|
||||
return new_tab
|
||||
|
||||
def remove_client_tab(self, tab: MDNavigationItemBase) -> None:
|
||||
"""
|
||||
Called to remove a tab and its screen.
|
||||
|
||||
:param tab: The tab to remove.
|
||||
"""
|
||||
tab_index = self.tabs.children.index(tab)
|
||||
# if the tab is currently active we need to swap before removing it
|
||||
if tab == self.screens.current_tab:
|
||||
if not tab_index:
|
||||
# account for the divider
|
||||
swap_index = tab_index + 2
|
||||
else:
|
||||
swap_index = tab_index - 2
|
||||
self.tabs.children[swap_index].on_release()
|
||||
# self.screens.switch_screens(self.tabs.children[swap_index])
|
||||
# get the divider to the left if we can
|
||||
if not tab_index:
|
||||
divider_index = tab_index + 1
|
||||
else:
|
||||
divider_index = tab_index - 1
|
||||
self.tabs.remove_widget(self.tabs.children[divider_index])
|
||||
self.tabs.remove_widget(tab)
|
||||
self.screens.remove_widget(self.screens.get_screen(tab.text))
|
||||
|
||||
def update_texts(self, dt):
|
||||
for slide in self.tabs.carousel.slides:
|
||||
if hasattr(slide, "fix_heights"):
|
||||
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if hasattr(self.screens.current_tab.content, "fix_heights"):
|
||||
getattr(self.screens.current_tab.content, "fix_heights")()
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
f" | Connected to: {self.ctx.server_address} " \
|
||||
|
||||
@@ -158,6 +158,7 @@ class APContainer:
|
||||
class APPlayerContainer(APContainer):
|
||||
"""A zipfile containing at least archipelago.json meant for a player"""
|
||||
game: ClassVar[Optional[str]] = None
|
||||
patch_file_ending: str = ""
|
||||
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
@@ -184,6 +185,7 @@ class APPlayerContainer(APContainer):
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
"patch_file_ending": self.patch_file_ending,
|
||||
})
|
||||
return manifest
|
||||
|
||||
@@ -223,7 +225,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
||||
"""
|
||||
hash: Optional[str] # base checksum of source file
|
||||
source_data: bytes
|
||||
patch_file_ending: str = ""
|
||||
files: Dict[str, bytes]
|
||||
|
||||
@classmethod
|
||||
@@ -245,7 +246,6 @@ class APProcedurePatch(APAutoPatchInterface):
|
||||
manifest = super(APProcedurePatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
manifest["procedure"] = self.procedure
|
||||
if self.procedure == APDeltaPatch.procedure:
|
||||
manifest["compatible_version"] = 5
|
||||
|
||||
@@ -210,16 +210,17 @@ components: List[Component] = [
|
||||
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
|
||||
# Core
|
||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||
Component('Generate', 'Generate', cli=True),
|
||||
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip'),
|
||||
description="Host a generated multiworld on your computer."),
|
||||
Component('Generate', 'Generate', cli=True,
|
||||
description="Generate a multiworld with the YAMLs in the players folder."),
|
||||
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld"),
|
||||
description="Install an APWorld to play games not included with Archipelago by default."),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
|
||||
description="Connect to a multiworld using the text client."),
|
||||
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')),
|
||||
@@ -242,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'),
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ def launch_client(*args) -> None:
|
||||
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier())
|
||||
file_identifier=SuffixIdentifier(),
|
||||
description="Open the BizHawk client, to play games using the Bizhawk emulator.")
|
||||
components.append(component)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||
import zipfile
|
||||
from BaseClasses import Location
|
||||
from worlds.Files import APContainer, AutoPatchRegister
|
||||
from worlds.Files import APPlayerContainer
|
||||
|
||||
from .Enum import CivVICheckType
|
||||
from .Locations import CivVILocation, CivVILocationData
|
||||
@@ -26,22 +25,19 @@ class CivTreeItem:
|
||||
ui_tree_row: int
|
||||
|
||||
|
||||
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
class CivVIContainer(APPlayerContainer):
|
||||
"""
|
||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||
"""
|
||||
game: Optional[str] = "Civilization VI"
|
||||
patch_file_ending = ".apcivvi"
|
||||
|
||||
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
|
||||
def __init__(self, patch_data: Dict[str, str], base_path: str = "", output_directory: str = "",
|
||||
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||
if isinstance(patch_data, io.BytesIO):
|
||||
super().__init__(patch_data, player, player_name, server)
|
||||
else:
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
for filename, yml in self.patch_data.items():
|
||||
|
||||
@@ -2893,3 +2893,18 @@ dog_bite_ice_trap_fix = [
|
||||
0x25291CB8, # ADDIU T1, T1, 0x1CB8
|
||||
0x01200008 # JR T1
|
||||
]
|
||||
|
||||
shimmy_speed_modifier = [
|
||||
# Increases the player's speed while shimmying as long as they are not holding down Z. If they are holding Z, it
|
||||
# will be the normal speed, allowing it to still be used to set up any tricks that might require the normal speed
|
||||
# (like Left Tower Skip).
|
||||
0x3C088038, # LUI T0, 0x8038
|
||||
0x91087D7E, # LBU T0, 0x7D7E (T0)
|
||||
0x31090020, # ANDI T1, T0, 0x0020
|
||||
0x3C0A800A, # LUI T2, 0x800A
|
||||
0x240B005A, # ADDIU T3, R0, 0x005A
|
||||
0x55200001, # BNEZL T1, [forward 0x01]
|
||||
0x240B0032, # ADDIU T3, R0, 0x0032
|
||||
0xA14B3641, # SB T3, 0x3641 (T2)
|
||||
0x0800B7C3 # J 0x8002DF0C
|
||||
]
|
||||
|
||||
@@ -424,6 +424,7 @@ class PantherDash(Choice):
|
||||
class IncreaseShimmySpeed(Toggle):
|
||||
"""
|
||||
Increases the speed at which characters shimmy left and right while hanging on ledges.
|
||||
Hold Z to use the regular speed in case it's needed to do something.
|
||||
"""
|
||||
display_name = "Increase Shimmy Speed"
|
||||
|
||||
|
||||
@@ -607,9 +607,10 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
|
||||
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
|
||||
|
||||
# Increase shimmy speed
|
||||
# Shimmy speed increase hack
|
||||
if options["increase_shimmy_speed"]:
|
||||
rom_data.write_byte(0xA4241, 0x5A)
|
||||
rom_data.write_int32(0x97EB4, 0x803FE9F0)
|
||||
rom_data.write_int32s(0xBFE9F0, patches.shimmy_speed_modifier)
|
||||
|
||||
# Disable landing fall damage
|
||||
if options["fall_guard"]:
|
||||
|
||||
@@ -211,7 +211,8 @@ class CVCotMWorld(World):
|
||||
"ignore_cleansing": self.options.ignore_cleansing.value,
|
||||
"skip_tutorials": self.options.skip_tutorials.value,
|
||||
"required_last_keys": self.required_last_keys,
|
||||
"completion_goal": self.options.completion_goal.value}
|
||||
"completion_goal": self.options.completion_goal.value,
|
||||
"nerf_roc_wing": self.options.nerf_roc_wing.value}
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(FILLER_ITEM_NAMES)
|
||||
|
||||
@@ -48,11 +48,17 @@ class OtherGameAppearancesInfo(TypedDict):
|
||||
|
||||
|
||||
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
|
||||
# NOTE: Symphony of the Night is currently an unsupported world not in main.
|
||||
# NOTE: Symphony of the Night and Harmony of Dissonance are custom worlds that are not core verified.
|
||||
"Symphony of the Night": {"Life Vessel": {"type": 0xE4,
|
||||
"appearance": 0x01},
|
||||
"Heart Vessel": {"type": 0xE4,
|
||||
"appearance": 0x00}},
|
||||
|
||||
"Castlevania - Harmony of Dissonance": {"Life Max Up": {"type": 0xE4,
|
||||
"appearance": 0x01},
|
||||
"Heart Max Up": {"type": 0xE4,
|
||||
"appearance": 0x00}},
|
||||
|
||||
"Timespinner": {"Max HP": {"type": 0xE4,
|
||||
"appearance": 0x01},
|
||||
"Max Aura": {"type": 0xE4,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Quick Links
|
||||
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
|
||||
- [Options Page](/games/Castlevania%20-%20Circle%20of%20the%20Moon/player-options)
|
||||
- [PopTracker Pack](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest)
|
||||
- [PopTracker Pack](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest)
|
||||
- [Repo for the original, standalone CotMR](https://github.com/calm-palm/cotm-randomizer)
|
||||
- [Web version of the above randomizer](https://rando.circleofthemoon.com/)
|
||||
- [A more in-depth guide to CotMR's nuances](https://docs.google.com/document/d/1uot4BD9XW7A--A8ecgoY8mLK_vSoQRpY5XCkzgas87c/view?usp=sharing)
|
||||
|
||||
@@ -22,7 +22,7 @@ clear it.
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest), for use with
|
||||
- [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest), for use with
|
||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
|
||||
## Generating and Patching a Game
|
||||
@@ -64,7 +64,7 @@ perfectly safe to make progress offline; everything will re-sync when you reconn
|
||||
|
||||
Castlevania: Circle of the Moon has a fully functional map tracker that supports auto-tracking.
|
||||
|
||||
1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest) and
|
||||
1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest) and
|
||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Put the tracker pack into `packs/` in your PopTracker install.
|
||||
3. Open PopTracker, and load the Castlevania: Circle of the Moon pack.
|
||||
|
||||
@@ -884,7 +884,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"),
|
||||
DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
|
||||
DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire",
|
||||
"Twin Dragon Greatshield"),
|
||||
"Twin Dragon Greatshield", missable=True), # After Eclipse
|
||||
DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood",
|
||||
hidden=True), # Hidden fall
|
||||
DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe",
|
||||
@@ -1887,7 +1887,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2",
|
||||
"Twinkling Titanite", lizard=True),
|
||||
DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby",
|
||||
miniboss=True), # Deep Accursed drop
|
||||
miniboss=True, missable=True), # Deep Accursed drop, missable after defeating Aldrich
|
||||
DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful",
|
||||
hidden=True), # Behind illusory wall
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
@@ -705,7 +709,7 @@ class DarkSouls3World(World):
|
||||
if self._is_location_available("US: Young White Branch - by white tree #2"):
|
||||
self._add_item_rule(
|
||||
"US: Young White Branch - by white tree #2",
|
||||
lambda item: item.player == self.player and not item.data.unique
|
||||
lambda item: item.player != self.player or not item.data.unique
|
||||
)
|
||||
|
||||
# Make sure the Storm Ruler is available BEFORE Yhorm the Giant
|
||||
@@ -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
|
||||
|
||||
@@ -802,8 +802,10 @@ def connect_regions(world: World, level_list):
|
||||
for i in range(0, len(kremwood_forest_levels) - 1):
|
||||
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
|
||||
|
||||
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
|
||||
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
|
||||
connection = connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
|
||||
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
|
||||
world.multiworld.register_indirect_condition(world.get_location(LocationName.riverside_race_flag).parent_region,
|
||||
connection)
|
||||
|
||||
# Cotton-Top Cove Connections
|
||||
cotton_top_cove_levels = [
|
||||
@@ -837,8 +839,11 @@ def connect_regions(world: World, level_list):
|
||||
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
||||
lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
|
||||
else:
|
||||
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
|
||||
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
|
||||
connection = connect(world, world.player, names, LocationName.mekanos_region,
|
||||
LocationName.sky_high_secret_region,
|
||||
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
|
||||
world.multiworld.register_indirect_condition(world.get_location(LocationName.bleaks_house).parent_region,
|
||||
connection)
|
||||
|
||||
# K3 Connections
|
||||
k3_levels = [
|
||||
@@ -946,3 +951,4 @@ def connect(world: World, player: int, used_names: typing.Dict[str, int], source
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
return connection
|
||||
|
||||
@@ -280,16 +280,19 @@ def set_boss_door_requirements_rules(player, world):
|
||||
set_rule(world.get_entrance("Boss Door", player), has_3_swords)
|
||||
|
||||
|
||||
def set_lfod_self_obtained_items_rules(world_options, player, world):
|
||||
def set_lfod_self_obtained_items_rules(world_options, player, multiworld):
|
||||
if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
|
||||
return
|
||||
set_rule(world.get_entrance("Vines", player),
|
||||
world = multiworld.worlds[player]
|
||||
set_rule(world.get_entrance("Vines"),
|
||||
lambda state: state.has("Incredibly Important Pack", player))
|
||||
set_rule(world.get_entrance("Behind Rocks", player),
|
||||
set_rule(world.get_entrance("Behind Rocks"),
|
||||
lambda state: state.can_reach("Cut Content", 'region', player))
|
||||
set_rule(world.get_entrance("Pickaxe Hard Cave", player),
|
||||
multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Behind Rocks"))
|
||||
set_rule(world.get_entrance("Pickaxe Hard Cave"),
|
||||
lambda state: state.can_reach("Cut Content", 'region', player) and
|
||||
state.has("Name Change Pack", player))
|
||||
multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Pickaxe Hard Cave"))
|
||||
|
||||
|
||||
def set_lfod_shuffled_items_rules(world_options, player, world):
|
||||
|
||||
@@ -69,7 +69,9 @@ class FactorioContext(CommonContext):
|
||||
# updated by spinup server
|
||||
mod_version: Version = Version(0, 0, 0)
|
||||
|
||||
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool):
|
||||
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool,
|
||||
rcon_port: int, rcon_password: str, server_settings_path: str | None,
|
||||
factorio_server_args: tuple[str, ...]):
|
||||
super(FactorioContext, self).__init__(server_address, password)
|
||||
self.send_index: int = 0
|
||||
self.rcon_client = None
|
||||
@@ -82,6 +84,10 @@ class FactorioContext(CommonContext):
|
||||
self.filter_item_sends: bool = filter_item_sends
|
||||
self.multiplayer: bool = False # whether multiple different players have connected
|
||||
self.bridge_chat_out: bool = bridge_chat_out
|
||||
self.rcon_port: int = rcon_port
|
||||
self.rcon_password: str = rcon_password
|
||||
self.server_settings_path: str = server_settings_path
|
||||
self.additional_factorio_server_args = factorio_server_args
|
||||
|
||||
@property
|
||||
def energylink_key(self) -> str:
|
||||
@@ -126,6 +132,18 @@ class FactorioContext(CommonContext):
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
|
||||
@property
|
||||
def server_args(self) -> tuple[str, ...]:
|
||||
if self.server_settings_path:
|
||||
return (
|
||||
"--rcon-port", str(self.rcon_port),
|
||||
"--rcon-password", self.rcon_password,
|
||||
"--server-settings", self.server_settings_path,
|
||||
*self.additional_factorio_server_args)
|
||||
else:
|
||||
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
|
||||
*self.additional_factorio_server_args)
|
||||
|
||||
@property
|
||||
def energy_link_status(self) -> str:
|
||||
if not self.energy_link_increment:
|
||||
@@ -311,7 +329,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
executable, "--create", savegame_name, "--preset", "archipelago"
|
||||
))
|
||||
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
|
||||
*(str(elem) for elem in server_args)),
|
||||
*ctx.server_args),
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
@@ -331,7 +349,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_queue.task_done()
|
||||
|
||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password,
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password,
|
||||
timeout=5)
|
||||
if not ctx.server:
|
||||
logger.info("Established bridge to Factorio Server. "
|
||||
@@ -422,7 +440,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
executable, "--create", savegame_name
|
||||
))
|
||||
factorio_process = subprocess.Popen(
|
||||
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
|
||||
(executable, "--start-server", savegame_name, *ctx.server_args),
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
@@ -451,7 +469,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
"or a Factorio sharing data directories is already running. "
|
||||
"Server could not start up.")
|
||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password)
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
await get_info(ctx, rcon_client)
|
||||
@@ -474,9 +492,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def main(args, filter_item_sends: bool, filter_bridge_chat_out: bool):
|
||||
ctx = FactorioContext(args.connect, args.password, filter_item_sends, filter_bridge_chat_out)
|
||||
|
||||
async def main(make_context):
|
||||
ctx = make_context()
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
@@ -509,38 +526,42 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
|
||||
return self._handle_text(node)
|
||||
|
||||
|
||||
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Refer to Factorio --help for those.")
|
||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
rcon_port = args.rcon_port
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||
random.choice(string.ascii_letters) for x in range(32))
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
settings: FactorioSettings = get_settings().factorio_options
|
||||
if os.path.samefile(settings.executable, sys.executable):
|
||||
selected_executable = settings.executable
|
||||
settings.executable = FactorioSettings.executable # reset to default
|
||||
raise Exception(f"FactorioClient was set to run itself {selected_executable}, aborting process bomb.")
|
||||
raise Exception(f"Factorio Client was set to run itself {selected_executable}, aborting process bomb.")
|
||||
|
||||
executable = settings.executable
|
||||
|
||||
server_settings = args.server_settings if args.server_settings \
|
||||
else getattr(settings, "server_settings", None)
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||
|
||||
|
||||
def launch():
|
||||
def launch(*new_args: str):
|
||||
import colorama
|
||||
global executable, server_settings, server_args
|
||||
global executable
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
# args handling
|
||||
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Refer to Factorio --help for those.")
|
||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
|
||||
|
||||
args, rest = parser.parse_known_args(args=new_args)
|
||||
rcon_port = args.rcon_port
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(32))
|
||||
|
||||
server_settings = args.server_settings if args.server_settings \
|
||||
else getattr(settings, "server_settings", None)
|
||||
|
||||
if server_settings:
|
||||
server_settings = os.path.abspath(server_settings)
|
||||
if not os.path.isfile(server_settings):
|
||||
raise FileNotFoundError(f"Could not find file {server_settings} for server_settings. Aborting.")
|
||||
|
||||
initial_filter_item_sends = bool(settings.filter_item_sends)
|
||||
initial_bridge_chat_out = bool(settings.bridge_chat_out)
|
||||
|
||||
@@ -554,14 +575,9 @@ def launch():
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
if server_settings and os.path.isfile(server_settings):
|
||||
server_args = (
|
||||
"--rcon-port", rcon_port,
|
||||
"--rcon-password", rcon_password,
|
||||
"--server-settings", server_settings,
|
||||
*rest)
|
||||
else:
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
|
||||
|
||||
asyncio.run(main(args, initial_filter_item_sends, initial_bridge_chat_out))
|
||||
asyncio.run(main(lambda: FactorioContext(
|
||||
args.connect, args.password,
|
||||
initial_filter_item_sends, initial_bridge_chat_out,
|
||||
rcon_port, rcon_password, server_settings, rest
|
||||
)))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -67,6 +67,7 @@ class FactorioModFile(worlds.Files.APPlayerContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
||||
patch_file_ending = ".zip"
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -22,9 +22,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
from .settings import FactorioSettings
|
||||
|
||||
|
||||
def launch_client():
|
||||
def launch_client(*args: str):
|
||||
from .Client import launch
|
||||
launch_component(launch, name="FactorioClient")
|
||||
launch_component(launch, name="Factorio Client", args=args)
|
||||
|
||||
|
||||
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -34,9 +34,9 @@ from .locations import (JakAndDaxterLocation,
|
||||
cache_location_table,
|
||||
orb_location_table)
|
||||
from .regions import create_regions
|
||||
from .rules import (enforce_multiplayer_limits,
|
||||
enforce_singleplayer_limits,
|
||||
verify_orb_trade_amounts,
|
||||
from .rules import (enforce_mp_absolute_limits,
|
||||
enforce_mp_friendly_limits,
|
||||
enforce_sp_limits,
|
||||
set_orb_trade_rule)
|
||||
from .locs import (cell_locations as cells,
|
||||
scout_locations as scouts,
|
||||
@@ -258,18 +258,31 @@ class JakAndDaxterWorld(World):
|
||||
self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1]
|
||||
self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2]
|
||||
|
||||
# Store this for remove function.
|
||||
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
||||
|
||||
# For the fairness of other players in a multiworld game, enforce some friendly limitations on our options,
|
||||
# so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen.
|
||||
# We would have done this earlier, but we needed to sort the power cell thresholds first.
|
||||
# We would have done this earlier, but we needed to sort the power cell thresholds first. Don't worry, we'll
|
||||
# come back to them.
|
||||
enforce_friendly_options = self.settings.enforce_friendly_options
|
||||
if enforce_friendly_options:
|
||||
if self.multiworld.players > 1:
|
||||
enforce_multiplayer_limits(self)
|
||||
if self.multiworld.players == 1:
|
||||
# For singleplayer games, always enforce/clamp the cell counts to valid values.
|
||||
enforce_sp_limits(self)
|
||||
else:
|
||||
if enforce_friendly_options:
|
||||
# For multiplayer games, we have a host setting to make options fair/sane for other players.
|
||||
# If this setting is enabled, enforce/clamp some friendly limitations on our options.
|
||||
enforce_mp_friendly_limits(self)
|
||||
else:
|
||||
enforce_singleplayer_limits(self)
|
||||
# Even if the setting is disabled, some values must be clamped to avoid generation errors.
|
||||
enforce_mp_absolute_limits(self)
|
||||
|
||||
# That's right, set the collection of thresholds again. Don't just clamp the values without updating this list!
|
||||
self.power_cell_thresholds = [
|
||||
self.options.fire_canyon_cell_count.value,
|
||||
self.options.mountain_pass_cell_count.value,
|
||||
self.options.lava_tube_cell_count.value,
|
||||
100, # The 100 Power Cell Door.
|
||||
]
|
||||
|
||||
# Now that the threshold list is finalized, store this for the remove function.
|
||||
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
||||
|
||||
# Calculate the number of power cells needed for full region access, the number being replaced by traps,
|
||||
# and the number of remaining filler.
|
||||
@@ -282,11 +295,6 @@ class JakAndDaxterWorld(World):
|
||||
self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells
|
||||
self.total_filler_cells = non_prog_cells - self.total_trap_cells
|
||||
|
||||
# Verify that we didn't overload the trade amounts with more orbs than exist in the world.
|
||||
# This is easy to do by accident even in a singleplayer world.
|
||||
self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount)
|
||||
verify_orb_trade_amounts(self)
|
||||
|
||||
# Cache the orb bundle size and item name for quicker reference.
|
||||
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
|
||||
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
- [What do Traps do?](#what-do-traps-do)
|
||||
- [What kind of Traps are there?](#what-kind-of-traps-are-there)
|
||||
- [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here)
|
||||
- [Why did I get an Option Error when generating a seed, and how do I fix it?](#why-did-i-get-an-option-error-when-generating-a-seed-and-how-do-i-fix-it)
|
||||
- [How do I generate seeds with 1 Orb Orbsanity and other extreme options?](#how-do-i-generate-seeds-with-1-orb-orbsanity-and-other-extreme-options)
|
||||
- [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game)
|
||||
- [How does the HUD work?](#how-does-the-hud-work)
|
||||
- [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it)
|
||||
@@ -201,16 +201,19 @@ Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `W
|
||||
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
|
||||
to the nearest sage's hut to continue your journey.
|
||||
|
||||
## Why did I get an Option Error when generating a seed and how do I fix it
|
||||
## How do I generate seeds with 1 orb orbsanity and other extreme options?
|
||||
Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or
|
||||
disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo
|
||||
game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have
|
||||
Singleplayer and Multiplayer Minimums and Maximums, collectively called "friendly limits."
|
||||
"friendly limits" that prevent you from choosing more extreme values.
|
||||
|
||||
If you're generating a solo game, or your multiworld host agrees to your request, you can override those limits by
|
||||
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`,
|
||||
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for
|
||||
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!**
|
||||
You can override **some**, not all, of those limits by editing the `host.yaml`. In the Archipelago Launcher, click
|
||||
`Open host.yaml`, then search for `jakanddaxter_options`, then search for `enforce_friendly_options`, then change this
|
||||
value from `true` to `false`. You can then generate a seed locally, and upload that to the Archipelago website to host
|
||||
for you (or host it yourself).
|
||||
|
||||
**Remember:** disabling this setting allows for more disruptive and challenging options, but it may cause seed
|
||||
generation to fail. **Use at your own risk!**
|
||||
|
||||
## How do I check my player options in-game
|
||||
When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
|
||||
- [The OpenGOAL Launcher](https://opengoal.dev/)
|
||||
- [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases)
|
||||
|
||||
At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux.
|
||||
|
||||
@@ -75,7 +74,7 @@ If you are in the middle of an async game, and you do not want to update the mod
|
||||
### New Game
|
||||
|
||||
- Run the Archipelago Launcher.
|
||||
- From the right-most list, find and click `Jak and Daxter Client`.
|
||||
- From the client list, find and click `Jak and Daxter Client`.
|
||||
- 3 new windows should appear:
|
||||
- The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile.
|
||||
- You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section.
|
||||
|
||||
@@ -1,22 +1,78 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter
|
||||
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter, \
|
||||
AssembleOptions
|
||||
from .items import trap_item_table
|
||||
|
||||
|
||||
class StaticGetter:
|
||||
def __init__(self, func):
|
||||
self.fget = func
|
||||
class readonly_classproperty:
|
||||
"""This decorator is used for getting friendly or unfriendly range_end values for options like FireCanyonCellCount
|
||||
and CitizenOrbTradeAmount. We only need to provide a getter as we will only be setting a single int to one of two
|
||||
values."""
|
||||
def __init__(self, getter):
|
||||
self.getter = getter
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self.fget(owner)
|
||||
return self.getter(owner)
|
||||
|
||||
|
||||
@StaticGetter
|
||||
@readonly_classproperty
|
||||
def determine_range_end(cls) -> int:
|
||||
from . import JakAndDaxterWorld
|
||||
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum
|
||||
from . import JakAndDaxterWorld # Avoid circular imports.
|
||||
friendly = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||
return cls.friendly_maximum if friendly else cls.absolute_maximum
|
||||
|
||||
|
||||
class classproperty:
|
||||
"""This decorator (?) is used for getting and setting friendly or unfriendly option values for the Orbsanity
|
||||
options."""
|
||||
def __init__(self, getter, setter):
|
||||
self.getter = getter
|
||||
self.setter = setter
|
||||
|
||||
def __get__(self, obj, value):
|
||||
return self.getter(obj)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
self.setter(obj, value)
|
||||
|
||||
|
||||
class AllowedChoiceMeta(AssembleOptions):
|
||||
"""This metaclass overrides AssembleOptions and provides inheriting classes a way to filter out "disallowed" values
|
||||
by way of implementing get_disallowed_options. This function is used by Jak and Daxter to check host.yaml settings
|
||||
without circular imports or breaking the settings API."""
|
||||
_name_lookup: dict[int, str]
|
||||
_options: dict[str, int]
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
ret = super().__new__(mcs, name, bases, attrs)
|
||||
ret._name_lookup = attrs["name_lookup"]
|
||||
ret._options = attrs["options"]
|
||||
return ret
|
||||
|
||||
def set_name_lookup(cls, value : dict[int, str]):
|
||||
cls._name_lookup = value
|
||||
|
||||
def get_name_lookup(cls) -> dict[int, str]:
|
||||
cls._name_lookup = {k: v for k, v in cls._name_lookup.items() if k not in cls.get_disallowed_options()}
|
||||
return cls._name_lookup
|
||||
|
||||
def set_options(cls, value: dict[str, int]):
|
||||
cls._options = value
|
||||
|
||||
def get_options(cls) -> dict[str, int]:
|
||||
cls._options = {k: v for k, v in cls._options.items() if v not in cls.get_disallowed_options()}
|
||||
return cls._options
|
||||
|
||||
def get_disallowed_options(cls):
|
||||
return {}
|
||||
|
||||
name_lookup = classproperty(get_name_lookup, set_name_lookup)
|
||||
options = classproperty(get_options, set_options)
|
||||
|
||||
|
||||
class AllowedChoice(Choice, metaclass=AllowedChoiceMeta):
|
||||
pass
|
||||
|
||||
|
||||
class EnableMoveRandomizer(Toggle):
|
||||
@@ -44,12 +100,13 @@ class EnableOrbsanity(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class GlobalOrbsanityBundleSize(Choice):
|
||||
class GlobalOrbsanityBundleSize(AllowedChoice):
|
||||
"""The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global."
|
||||
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
|
||||
|
||||
Multiplayer Minimum: 10
|
||||
Multiplayer Maximum: 200"""
|
||||
This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
|
||||
non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
|
||||
in host.yaml."""
|
||||
display_name = "Global Orbsanity Bundle Size"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
@@ -75,12 +132,33 @@ class GlobalOrbsanityBundleSize(Choice):
|
||||
friendly_maximum = 200
|
||||
default = 20
|
||||
|
||||
@classmethod
|
||||
def get_disallowed_options(cls) -> set[int]:
|
||||
try:
|
||||
from . import JakAndDaxterWorld
|
||||
if JakAndDaxterWorld.settings.enforce_friendly_options:
|
||||
return {cls.option_1_orb,
|
||||
cls.option_2_orbs,
|
||||
cls.option_4_orbs,
|
||||
cls.option_5_orbs,
|
||||
cls.option_8_orbs,
|
||||
cls.option_250_orbs,
|
||||
cls.option_400_orbs,
|
||||
cls.option_500_orbs,
|
||||
cls.option_1000_orbs,
|
||||
cls.option_2000_orbs}
|
||||
except ImportError:
|
||||
pass
|
||||
return set()
|
||||
|
||||
class PerLevelOrbsanityBundleSize(Choice):
|
||||
|
||||
class PerLevelOrbsanityBundleSize(AllowedChoice):
|
||||
"""The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level."
|
||||
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.
|
||||
|
||||
Multiplayer Minimum: 10"""
|
||||
This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
|
||||
non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
|
||||
in host.yaml."""
|
||||
display_name = "Per Level Orbsanity Bundle Size"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
@@ -91,6 +169,18 @@ class PerLevelOrbsanityBundleSize(Choice):
|
||||
friendly_minimum = 10
|
||||
default = 25
|
||||
|
||||
@classmethod
|
||||
def get_disallowed_options(cls) -> set[int]:
|
||||
try:
|
||||
from . import JakAndDaxterWorld
|
||||
if JakAndDaxterWorld.settings.enforce_friendly_options:
|
||||
return {cls.option_1_orb,
|
||||
cls.option_2_orbs,
|
||||
cls.option_5_orbs}
|
||||
except ImportError:
|
||||
pass
|
||||
return set()
|
||||
|
||||
|
||||
class FireCanyonCellCount(Range):
|
||||
"""The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to
|
||||
@@ -234,7 +324,7 @@ class CompletionCondition(Choice):
|
||||
option_cross_fire_canyon = 69
|
||||
option_cross_mountain_pass = 87
|
||||
option_cross_lava_tube = 89
|
||||
option_defeat_dark_eco_plant = 6
|
||||
# option_defeat_dark_eco_plant = 6
|
||||
option_defeat_klaww = 86
|
||||
option_defeat_gol_and_maia = 112
|
||||
option_open_100_cell_door = 116
|
||||
|
||||
@@ -115,8 +115,8 @@ def create_regions(world: "JakAndDaxterWorld"):
|
||||
elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||
# elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||
# multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
import math
|
||||
import typing
|
||||
from BaseClasses import CollectionState
|
||||
from Options import OptionError
|
||||
@@ -131,100 +133,138 @@ def can_fight(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
|
||||
|
||||
|
||||
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"):
|
||||
def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
|
||||
options = world.options
|
||||
friendly_message = ""
|
||||
|
||||
if (options.enable_orbsanity == EnableOrbsanity.option_global
|
||||
and (options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum
|
||||
or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum)):
|
||||
friendly_message += (f" "
|
||||
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_minimum} and no greater than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_maximum} (currently "
|
||||
f"{options.global_orbsanity_bundle_size.value}).\n")
|
||||
|
||||
if (options.enable_orbsanity == EnableOrbsanity.option_per_level
|
||||
and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum):
|
||||
friendly_message += (f" "
|
||||
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently "
|
||||
f"{options.level_orbsanity_bundle_size.value}).\n")
|
||||
|
||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||
old_value = options.fire_canyon_cell_count.value
|
||||
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
||||
f"{options.fire_canyon_cell_count.value}).\n")
|
||||
f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
||||
old_value = options.mountain_pass_cell_count.value
|
||||
options.mountain_pass_cell_count.value = MountainPassCellCount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
||||
f"{options.mountain_pass_cell_count.value}).\n")
|
||||
f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
||||
old_value = options.lava_tube_cell_count.value
|
||||
options.lava_tube_cell_count.value = LavaTubeCellCount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
||||
f"{options.lava_tube_cell_count.value}).\n")
|
||||
f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
return friendly_message
|
||||
|
||||
|
||||
def clamp_trade_total_limits(world: "JakAndDaxterWorld"):
|
||||
"""Check if we need to recalculate the 2 trade orb options so the total fits under 2000. If so let's keep them
|
||||
proportional relative to each other. Then we'll recalculate total_trade_orbs. Remember this situation is
|
||||
only possible if both values are greater than 0, otherwise the absolute maximums would keep them under 2000."""
|
||||
options = world.options
|
||||
friendly_message = ""
|
||||
|
||||
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
|
||||
if world.total_trade_orbs > 2000:
|
||||
old_total = world.total_trade_orbs
|
||||
old_citizen_value = options.citizen_orb_trade_amount.value
|
||||
old_oracle_value = options.oracle_orb_trade_amount.value
|
||||
|
||||
coefficient = old_oracle_value / old_citizen_value
|
||||
|
||||
options.citizen_orb_trade_amount.value = math.floor(2000 / (9 + (6 * coefficient)))
|
||||
options.oracle_orb_trade_amount.value = math.floor(coefficient * options.citizen_orb_trade_amount.value)
|
||||
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
|
||||
|
||||
friendly_message += (f" "
|
||||
f"Required number of orbs ({old_total}) must be no greater than total orbs in the game "
|
||||
f"(2000). Reduced the value of {world.options.citizen_orb_trade_amount.display_name} "
|
||||
f"from {old_citizen_value} to {options.citizen_orb_trade_amount.value} and "
|
||||
f"{world.options.oracle_orb_trade_amount.display_name} from {old_oracle_value} to "
|
||||
f"{options.oracle_orb_trade_amount.value}.\n")
|
||||
|
||||
return friendly_message
|
||||
|
||||
|
||||
def enforce_mp_friendly_limits(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
friendly_message = ""
|
||||
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_global:
|
||||
if options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum:
|
||||
old_value = options.global_orbsanity_bundle_size.value
|
||||
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_minimum
|
||||
friendly_message += (f" "
|
||||
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum:
|
||||
old_value = options.global_orbsanity_bundle_size.value
|
||||
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.global_orbsanity_bundle_size.display_name} must be no greater than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
if options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum:
|
||||
old_value = options.level_orbsanity_bundle_size.value
|
||||
options.level_orbsanity_bundle_size.value = PerLevelOrbsanityBundleSize.friendly_minimum
|
||||
friendly_message += (f" "
|
||||
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
|
||||
old_value = options.citizen_orb_trade_amount.value
|
||||
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.citizen_orb_trade_amount.value}).\n")
|
||||
f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
|
||||
old_value = options.oracle_orb_trade_amount.value
|
||||
options.oracle_orb_trade_amount.value = OracleOrbTradeAmount.friendly_maximum
|
||||
friendly_message += (f" "
|
||||
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.oracle_orb_trade_amount.value}).\n")
|
||||
f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
|
||||
f"changed option to appropriate value.\n")
|
||||
|
||||
friendly_message += clamp_cell_limits(world)
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n"
|
||||
f"Please adjust the following Options for a multiplayer game. \n"
|
||||
f"{friendly_message}"
|
||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
||||
f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. "
|
||||
f"(Use at your own risk!)")
|
||||
logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
|
||||
f"{friendly_message}"
|
||||
f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
|
||||
f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
|
||||
|
||||
|
||||
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
|
||||
friendly_message = ""
|
||||
|
||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
||||
f"{options.fire_canyon_cell_count.value}).\n")
|
||||
|
||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
||||
f"{options.mountain_pass_cell_count.value}).\n")
|
||||
|
||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
||||
f"{options.lava_tube_cell_count.value}).\n")
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"The options you have chosen may result in seed generation failures. \n"
|
||||
f"Please adjust the following Options for a singleplayer game. \n"
|
||||
f"{friendly_message}"
|
||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
||||
f"Or set 'enforce_friendly_options' in your host.yaml to false. "
|
||||
f"(Use at your own risk!)")
|
||||
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
|
||||
f"{friendly_message}")
|
||||
|
||||
|
||||
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"):
|
||||
def enforce_sp_limits(world: "JakAndDaxterWorld"):
|
||||
friendly_message = ""
|
||||
|
||||
if world.total_trade_orbs > 2000:
|
||||
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) "
|
||||
f"is more than all the orbs in the game (2000). Reduce the value of either "
|
||||
f"{world.options.citizen_orb_trade_amount.display_name} "
|
||||
f"or {world.options.oracle_orb_trade_amount.display_name}.")
|
||||
friendly_message += clamp_cell_limits(world)
|
||||
friendly_message += clamp_trade_total_limits(world)
|
||||
|
||||
if friendly_message != "":
|
||||
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
|
||||
f"{friendly_message}")
|
||||
|
||||
@@ -4,14 +4,14 @@ from .bases import JakAndDaxterTestBase
|
||||
class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 0,
|
||||
"oracle_orb_trade_amount": 0
|
||||
}
|
||||
|
||||
def test_orb_items_are_filler(self):
|
||||
self.collect_all_but("")
|
||||
self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items)
|
||||
self.assertNotIn("10 Precursor Orbs", self.multiworld.state.prog_items)
|
||||
|
||||
def test_trades_are_accessible(self):
|
||||
self.assertTrue(self.multiworld
|
||||
@@ -22,15 +22,15 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||
class TradesCostEverythingTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 120,
|
||||
"oracle_orb_trade_amount": 150
|
||||
}
|
||||
|
||||
def test_orb_items_are_progression(self):
|
||||
self.collect_all_but("")
|
||||
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"])
|
||||
self.assertIn("10 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||
self.assertEqual(198, self.multiworld.state.prog_items[self.player]["10 Precursor Orbs"])
|
||||
|
||||
def test_trades_are_accessible(self):
|
||||
self.collect_all_but("")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ from worlds.Files import APPlayerContainer
|
||||
|
||||
class KH2Container(APPlayerContainer):
|
||||
game: str = 'Kingdom Hearts 2'
|
||||
patch_file_ending = ".zip"
|
||||
|
||||
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
|
||||
player=None, player_name: str = "", server: str = ""):
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -4956,10 +4956,16 @@
|
||||
Outside The Initiated:
|
||||
room: Art Gallery
|
||||
door: Exit
|
||||
The Bearer (East): True
|
||||
The Bearer (North): True
|
||||
The Bearer (South): True
|
||||
The Bearer (West): True
|
||||
The Bearer (East):
|
||||
static_painting: True
|
||||
The Bearer (North):
|
||||
static_painting: True
|
||||
The Bearer (South):
|
||||
static_painting: True
|
||||
The Bearer (West):
|
||||
- static_painting: True
|
||||
- room: The Bearer (West)
|
||||
door: Side Area Shortcut
|
||||
Roof: True
|
||||
panels:
|
||||
Achievement:
|
||||
@@ -5053,7 +5059,8 @@
|
||||
- MIDDLE
|
||||
The Bearer (East):
|
||||
entrances:
|
||||
Cross Tower (East): True
|
||||
Cross Tower (East):
|
||||
static_painting: True
|
||||
Bearer Side Area:
|
||||
door: Side Area Access
|
||||
Roof: True
|
||||
@@ -5084,7 +5091,8 @@
|
||||
panel: SPACE
|
||||
The Bearer (North):
|
||||
entrances:
|
||||
Cross Tower (East): True
|
||||
Cross Tower (North):
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
SILENT (1):
|
||||
@@ -5128,7 +5136,8 @@
|
||||
panel: POTS
|
||||
The Bearer (South):
|
||||
entrances:
|
||||
Cross Tower (North): True
|
||||
Cross Tower (South):
|
||||
static_painting: True
|
||||
Bearer Side Area:
|
||||
door: Side Area Shortcut
|
||||
Roof: True
|
||||
@@ -5162,7 +5171,10 @@
|
||||
panel: SILENT (1)
|
||||
The Bearer (West):
|
||||
entrances:
|
||||
Cross Tower (West): True
|
||||
Cross Tower (West):
|
||||
static_painting: True
|
||||
The Bearer:
|
||||
door: Side Area Shortcut
|
||||
Bearer Side Area:
|
||||
door: Side Area Shortcut
|
||||
Roof: True
|
||||
@@ -5235,6 +5247,7 @@
|
||||
The Bearer:
|
||||
room: The Bearer
|
||||
door: East Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
WINTER:
|
||||
@@ -5250,6 +5263,7 @@
|
||||
The Bearer (East):
|
||||
room: The Bearer (East)
|
||||
door: North Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
NORTH:
|
||||
@@ -5270,6 +5284,7 @@
|
||||
The Bearer (North):
|
||||
room: The Bearer (North)
|
||||
door: South Entrance
|
||||
static_painting: True
|
||||
panels:
|
||||
FIRE:
|
||||
id: Cross Room/Panel_fire_fire
|
||||
@@ -5284,6 +5299,7 @@
|
||||
Bearer Side Area:
|
||||
room: Bearer Side Area
|
||||
door: West Entrance
|
||||
static_painting: True
|
||||
Roof: True
|
||||
panels:
|
||||
DIAMONDS:
|
||||
@@ -7108,6 +7124,8 @@
|
||||
entrances:
|
||||
Orange Tower Third Floor:
|
||||
warp: True
|
||||
Art Gallery (First Floor):
|
||||
warp: True
|
||||
Art Gallery (Second Floor):
|
||||
warp: True
|
||||
Art Gallery (Third Floor):
|
||||
@@ -7125,22 +7143,6 @@
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
door: Eights
|
||||
EON:
|
||||
id: Painting Room/Panel_eon_one
|
||||
colors: yellow
|
||||
tag: midyellow
|
||||
TRUSTWORTHY:
|
||||
id: Painting Room/Panel_to_two
|
||||
colors: red
|
||||
tag: midred
|
||||
FREE:
|
||||
id: Painting Room/Panel_free_three
|
||||
colors: purple
|
||||
tag: midpurp
|
||||
OUR:
|
||||
id: Painting Room/Panel_our_four
|
||||
colors: blue
|
||||
tag: midblue
|
||||
ORDER:
|
||||
id: Painting Room/Panel_order_onepathmanyturns
|
||||
tag: forbid
|
||||
@@ -7159,15 +7161,8 @@
|
||||
- scenery_painting_2c
|
||||
skip_location: True
|
||||
panels:
|
||||
- EON
|
||||
First Floor Puzzles:
|
||||
skip_item: True
|
||||
location_name: Art Gallery - First Floor Puzzles
|
||||
panels:
|
||||
- EON
|
||||
- TRUSTWORTHY
|
||||
- FREE
|
||||
- OUR
|
||||
- room: Art Gallery (First Floor)
|
||||
panel: EON
|
||||
Third Floor:
|
||||
painting_id:
|
||||
- scenery_painting_3b
|
||||
@@ -7227,11 +7222,42 @@
|
||||
- Third Floor
|
||||
- Fourth Floor
|
||||
- Fifth Floor
|
||||
Art Gallery (First Floor):
|
||||
entrances:
|
||||
Art Gallery:
|
||||
static_painting: True
|
||||
panels:
|
||||
EON:
|
||||
id: Painting Room/Panel_eon_one
|
||||
colors: yellow
|
||||
tag: midyellow
|
||||
TRUSTWORTHY:
|
||||
id: Painting Room/Panel_to_two
|
||||
colors: red
|
||||
tag: midred
|
||||
FREE:
|
||||
id: Painting Room/Panel_free_three
|
||||
colors: purple
|
||||
tag: midpurp
|
||||
OUR:
|
||||
id: Painting Room/Panel_our_four
|
||||
colors: blue
|
||||
tag: midblue
|
||||
doors:
|
||||
Puzzles:
|
||||
skip_item: True
|
||||
location_name: Art Gallery - First Floor Puzzles
|
||||
panels:
|
||||
- EON
|
||||
- TRUSTWORTHY
|
||||
- FREE
|
||||
- OUR
|
||||
Art Gallery (Second Floor):
|
||||
entrances:
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Second Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
HOUSE:
|
||||
id: Painting Room/Panel_house_neighborhood
|
||||
@@ -7263,6 +7289,7 @@
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Third Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
AN:
|
||||
id: Painting Room/Panel_an_many
|
||||
@@ -7294,6 +7321,7 @@
|
||||
Art Gallery:
|
||||
room: Art Gallery
|
||||
door: Fourth Floor
|
||||
static_painting: True
|
||||
panels:
|
||||
URNS:
|
||||
id: Painting Room/Panel_urns_turns
|
||||
|
||||
Binary file not shown.
@@ -727,11 +727,12 @@ panels:
|
||||
WANDER: 444975
|
||||
Art Gallery:
|
||||
EIGHT: 444976
|
||||
ORDER: 444981
|
||||
Art Gallery (First Floor):
|
||||
EON: 444977
|
||||
TRUSTWORTHY: 444978
|
||||
FREE: 444979
|
||||
OUR: 444980
|
||||
ORDER: 444981
|
||||
Art Gallery (Second Floor):
|
||||
HOUSE: 444982
|
||||
PATH: 444983
|
||||
@@ -1382,8 +1383,6 @@ doors:
|
||||
Art Gallery:
|
||||
Second Floor:
|
||||
item: 444558
|
||||
First Floor Puzzles:
|
||||
location: 445256
|
||||
Third Floor:
|
||||
item: 444559
|
||||
Fourth Floor:
|
||||
@@ -1393,6 +1392,9 @@ doors:
|
||||
Exit:
|
||||
item: 444562
|
||||
location: 444981
|
||||
Art Gallery (First Floor):
|
||||
Puzzles:
|
||||
location: 445256
|
||||
Art Gallery (Second Floor):
|
||||
Puzzles:
|
||||
location: 445257
|
||||
|
||||
@@ -23,6 +23,7 @@ class EntranceType(Flag):
|
||||
SUNWARP = auto()
|
||||
WARP = auto()
|
||||
CROSSROADS_ROOF_ACCESS = auto()
|
||||
STATIC_PAINTING = auto()
|
||||
|
||||
|
||||
class RoomEntrance(NamedTuple):
|
||||
|
||||
@@ -30,7 +30,7 @@ def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "Lingo
|
||||
allowed_entrance_types = EntranceType.NORMAL
|
||||
|
||||
if world.options.pilgrimage_allows_paintings:
|
||||
allowed_entrance_types |= EntranceType.PAINTING
|
||||
allowed_entrance_types |= EntranceType.PAINTING | EntranceType.STATIC_PAINTING
|
||||
|
||||
if world.options.pilgrimage_allows_roof_access:
|
||||
allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
|
||||
@@ -105,7 +105,8 @@ def create_regions(world: "LingoWorld") -> None:
|
||||
regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
|
||||
|
||||
# Connect all created regions now that they exist.
|
||||
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS
|
||||
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS | \
|
||||
EntranceType.STATIC_PAINTING
|
||||
|
||||
if not painting_shuffle:
|
||||
# Don't use the vanilla painting connections if we are shuffling paintings.
|
||||
@@ -156,11 +157,11 @@ def create_regions(world: "LingoWorld") -> None:
|
||||
regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
|
||||
else:
|
||||
connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
|
||||
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world)
|
||||
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.STATIC_PAINTING, False, world)
|
||||
|
||||
if early_color_hallways:
|
||||
connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways",
|
||||
None, EntranceType.PAINTING, False, world)
|
||||
None, EntranceType.STATIC_PAINTING, False, world)
|
||||
|
||||
if painting_shuffle:
|
||||
for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
|
||||
|
||||
@@ -138,6 +138,8 @@ def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomE
|
||||
entrance_type = EntranceType.WARP
|
||||
elif source_room == "Crossroads" and room_name == "Roof":
|
||||
entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
|
||||
elif "static_painting" in door_obj and door_obj["static_painting"]:
|
||||
entrance_type = EntranceType.STATIC_PAINTING
|
||||
|
||||
if "painting" in door_obj and door_obj["painting"]:
|
||||
PAINTING_EXIT_ROOMS.add(room_name)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# the file are consistent. It also checks that the panel and door IDs mentioned
|
||||
# all exist in the map file.
|
||||
#
|
||||
# Usage: validate_config.rb [config file] [map file]
|
||||
# Usage: validate_config.rb [config file] [ids path] [map file]
|
||||
|
||||
require 'set'
|
||||
require 'yaml'
|
||||
|
||||
@@ -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.
@@ -38,6 +38,7 @@ AP_JUNK = 0xD5
|
||||
|
||||
class OoTContainer(APPatch):
|
||||
game: str = 'Ocarina of Time'
|
||||
patch_file_ending = ".apz5"
|
||||
|
||||
def __init__(self, patch_data: bytes, base_path: str, output_directory: str,
|
||||
player = None, player_name: str = "", server: str = ""):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user