Compare commits

..

37 Commits

Author SHA1 Message Date
NewSoupVi
da256d7252 Take Counter back out of RestrictedUnpickler 2025-07-07 01:53:16 +02:00
Doug Hoskisson
e68b1ad428 CommonClient: fix extra panels added to main_area_container (#5151) 2025-07-06 19:22:02 +02:00
Ixrec
072e2ece15 Docs: 'get_prefill_items' -> 'get_pre_fill_items' (#5167) 2025-07-05 17:01:08 -04:00
agilbert1412
11130037fe Stardew Valley: Fixed luck level requirements for slot machines #5160
# Conflicts:
#	worlds/stardew_valley/data/craftable_data.py
2025-07-03 21:08:36 +02:00
Scipio Wright
ba66ef14cc Update world api.md (#5149) 2025-07-02 14:14:35 +02:00
Jérémie Bolduc
8aacc23882 SDV: Add "Desert Transportation" and "Island Transportation" Item Groups (#5143) 2025-06-28 11:36:09 -04:00
Jonathan Tan
03e5fd3dae TWW: Fix Swords in Swordless Mode (#5137)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-28 10:46:37 -04:00
Fly Hyping
da52598c08 Wargroove: Fix Communication Thread (#5125) 2025-06-27 19:42:35 -04:00
Jonathan Tan
52389731eb TWW: Update Preset S7 to S8 (#5138) 2025-06-27 18:46:00 -04:00
LiquidCat64
21864f6f95 CVCotM: Fix Advance Collection ROM (#5132) 2025-06-27 18:25:45 -04:00
DJ-lennart
00f8625280 Civilization VI: Updated setup and info pages (#5123)
* Update setup_en.md

Updated setup instructions for Civilization VI in Archipelago

* Update en_Civilization VI.md

Updated info page for Civilization VI in Archipelago

* Update setup_en.md
2025-06-21 16:31:12 +02:00
James White
c34e29c712 Pokemon RB: Client: Send bounce messages with current map ID (#5121) 2025-06-20 22:52:54 +02:00
palex00
e0ae3359f1 Pokémon RB: Use new link for a new tracker (#5122)
* Update setup_en.md

* Update setup_es.md
2025-06-20 20:55:49 +02:00
Katelyn Gigante
c2666bacd7 core: Don't attempt to write to the inside of an OSX App Bundle (#4380)
* core: Frozen OSX should also use Home Directory

* Use Application Support instead of homedir

* Suggested changes
2025-06-19 18:05:52 +02:00
Aaron Wagener
4eefd9c3ce Kivy: swap from the tab carousel to navigation bar (#4930)
* implement tabs as NavigationBar

* update the underline bar with the screen manager

* remove some unneeded kv

* remove the underline in favor of a full tab highlight

* fix insert transitions

* use on_release instead of on_press

* minor cleanup

* add remove_client_tab and add a caller to the NavigationBar for back compat

* unused imports

* Update kvui.py

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-06-19 13:39:26 +02:00
Silvris
211456242e KDL3: update to gifting protocol 3 and update settings usage (#4814)
* gift version 3

* update settings usage

* that really has just been broken this entire time

* remove unnecessary print

* Update client.py

* fix random flavor handling

* fix incorrect sender/receiver

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-16 13:00:47 -04:00
massimilianodelliubaldini
6f244c4661 Docs: Update Plando Guide and Make it More User Friendly (#4858)
* Make plando guide more user friendly.

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Further updates for review.

* Clear search box when filtering by type.

* Forget previous commit name - more code review updates to doc.

* Move link to yaml tutorial.

* Replace STS example with Pokemon RB.

* Use non-key item examples in RB.

* Rooby's code review updates.

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Address some more feedback.

* Make Factorio example more accurate.

* Exempt's code review updates (round 4)

* Exempt's code review updates (round 4 + 1)

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/generic/docs/plando_en.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-16 12:54:08 -04:00
Exempt-Medic
47bf6d724b Minecraft Removal Cleanup (#5118) 2025-06-16 10:56:47 -04:00
Ixrec
5c710ad032 Docs: Rework the "Events" Section of world api.md (#5012)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-16 08:36:12 -04:00
BlastSlimey
dda5a05cbb shapez: Change Links to Shapesanity Cheat Sheet (#5047) 2025-06-16 08:07:27 -04:00
Natalie Weizenbaum
e0a63e0290 DS3: Link to the Appropriate .NET Runtime for Proton (#5093) 2025-06-16 08:02:06 -04:00
NewSoupVi
9246659589 Make sure ladx removes the same copy of the starting item from the itempool that it's placing (#5110) 2025-06-16 13:49:30 +02:00
digiholic
377cdb84b4 MMBN3: Fixes Generation Errors and General UX Smoothing (#5077)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-06-16 07:47:55 -04:00
KonoTyran
0e759f25fd Remove Minecraft (#4672)
* Remove Minecraft

* remove minecraft

* remove minecraft

* elif -> if

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-06-16 12:31:16 +02:00
qwint
b408bb4f6e Core: Docstring typo on Region.add_exits (#5089)
* doc typo

* Update BaseClasses.py
2025-06-16 02:31:12 +02:00
JusticePS
1356479415 AdventureClient: Replace Utils.get_settings with settings.get_settings #5043 2025-06-16 01:30:45 +02:00
Exempt-Medic
ec5b4e704f Plando Items: Better Warning for Nonexisting Worlds (#5112) 2025-06-14 09:28:02 -04:00
Exempt-Medic
aa9e617510 DS3: Apply Rules to Non-Randomized Locations (#5106) 2025-06-14 09:27:22 -04:00
Exempt-Medic
ecb739ce96 Plando Items: Fix Location Groups Unfolding (#5099) 2025-06-14 09:26:58 -04:00
Exempt-Medic
3b72140435 Shivers: Fix get_pre_fill_items (#5113) 2025-06-14 09:26:22 -04:00
Louis M
27a6770569 Aquaria: Fixing open waters urns not breakable with nature forms logic bug (#5072)
* Fixing open waters urns not breakable with nature forms logic bug

* Using list in comprehension only when useful

* Replacing damaging items by a constant

* Removing comprehension list creating from lambda
2025-06-14 13:17:33 +02:00
NewSoupVi
2ff611167a ALTTP: Fix take_any leaving a placed item in the multiworld itempool #5108 2025-06-14 12:21:25 +02:00
agilbert1412
e83e178b63 Stardew Valley: Fix 3 Logic Issues (#5094)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-13 20:29:23 -04:00
Exempt-Medic
068a757373 Item Plando: Fix count value (#5101) 2025-06-13 20:29:06 -04:00
PoryGone
0ad4527719 SA2B: Logic Fixes (#5095)
- Fixed King Boom Boo being able to appear in multiple boss gates
- `Final Rush - 16 Animals (Expert)` no longer requires `Sonic - Bounce Bracelet`
- `Dry Lagoon - 5 (Standard)` now requires `Rouge - Pick Nails`
- `Sand Ocean - Extra Life Box 2 (Standard/Hard/Expert)` no longer requires `Eggman - Jet Engine`
- `Security Hall - 8 Animals (Expert)` no longer requires `Rouge - Pick Nails`
- `Sky Rail - Item Box 8 (Standard)` now requires `Shadow - Air Shoes` and `Shadow - Mystic Melody`
- `Cosmic Wall - Chao Key 1 (Standard/Hard/Expert)` no longer requires `Eggman - Mystic Melody`
- `Cannon's Core - Pipe 2 (Expert)` no longer requires `Tails - Booster`
- `Cannon's Core - Gold Beetle` no longer requires `Tails - Booster` nor `Knuckles - Hammer Gloves`
2025-06-13 22:01:19 +02:00
qwint
8c6327d024 LTTP/SDV: use .name when appropriate in subtests (#5107) 2025-06-13 21:56:09 +02:00
qwint
aecbb2ab02 fix saving princess's use of subprocess helpers (#5103) 2025-06-13 12:28:58 +02:00
89 changed files with 953 additions and 5125 deletions

7
.gitignore vendored
View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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):

View File

@@ -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()

View File

@@ -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

View File

@@ -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()
@@ -437,9 +441,6 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by OptionCounter
if module == "collections" and name == "Counter":
return collections.Counter
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
"SlotType", "NetworkSlot", "HintStatus"}:

View File

@@ -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"):

View File

@@ -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;
});
}
});

View File

@@ -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;
}

View File

@@ -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 %}

View File

@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;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>

View File

@@ -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 = {

View File

@@ -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"):

View File

@@ -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"

View File

@@ -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')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -121,9 +121,6 @@
# The Messenger
/worlds/messenger/ @alwaysintreble
# Minecraft
/worlds/minecraft/ @KonoTyran @espeon65536
# Mega Man 2
/worlds/mm2/ @Silvris

View File

@@ -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

View File

@@ -258,31 +258,6 @@ another flag like "progression", it means "an especially useful progression item
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
### Events
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
never made aware of them and these locations can never be checked, nor can the items be received during play.
They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
They must not exist in the `name_to_id` lookups, as they have no ID.
The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
```python
from worlds.AutoWorld import World
from BaseClasses import ItemClassification
from .subclasses import MyGameLocation, MyGameItem
class MyGameWorld(World):
victory_loc = MyGameLocation(self.player, "Victory", None)
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
```
### Regions
Regions are logical containers that typically hold locations that share some common access rules. If location logic is
@@ -291,7 +266,7 @@ like entrance randomization in logic.
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L310-L311)),
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
### Entrances
@@ -339,6 +314,63 @@ avoiding the need for indirect conditions at the expense of performance.
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
reject the placement of an item there.
### Events (or "generation-only items/locations")
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
Event locations can never be checked by the player, and event items cannot be received during play.
Events are used to represent in-game actions (that aren't regular Archipelago locations) when either:
* We want to show in the spoiler log when the player is expected to perform the in-game action.
* It's the cleanest way to represent how that in-game action impacts logic.
Typical examples include completing the goal, defeating a boss, or flipping a switch that affects multiple areas.
To be precise: the term "event" on its own refers to the special combination of an "event item" placed on an "event
location". Event items and locations are created the same way as normal items and locations, except that they have an
`id` of `None`, and an event item must be placed on an event location
(and vice versa). Finally, although events are often described as "fake" items and locations, it's important to
understand that they are perfectly real during generation.
The most common way to create an event is to create the event item and the event location, then immediately call
`Location.place_locked_item()`:
```python
victory_loc = MyGameLocation(self.player, "Defeat the Final Boss", None, final_boss_arena_region)
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
set_rule(victory_loc, lambda state: state.has("Boss Defeating Sword", self.player))
```
Requiring an event to finish the game will make the spoiler log display an additional
`Defeat the Final Boss: Victory` line when the player is expected to finish, rather than only showing their last
relevant item. But events aren't just about the spoiler log; a more substantial example of using events to structure
your logic might be:
```python
water_loc = MyGameLocation(self.player, "Water Level Switch", None, pump_station_region)
water_loc.place_locked_item(MyGameItem("Lowered Water Level", ItemClassification.progression, None, self.player))
pump_station_region.locations.append(water_loc)
set_rule(water_loc, lambda state: state.has("Double Jump", self.player)) # the switch is really high up
...
basement_loc = MyGameLocation(self.player, "Flooded House - Basement Chest", None, flooded_house_region)
flooded_house_region.locations += [upstairs_loc, ground_floor_loc, basement_loc]
...
set_rule(basement_loc, lambda state: state.has("Lowered Water Level", self.player))
```
This creates a "Lowered Water Level" event and a regular location whose access rule depends on that
event being reachable. If you made several more locations the same way, this would ensure all of those locations can
only become reachable when the event location is reachable (i.e. when the water level can be lowered), without
copy-pasting the event location's access rule and then repeatedly re-evaluating it. Also, the spoiler log will show
`Water Level Switch: Lowered Water Level` when the player is expected to do this.
To be clear, this example could also be modeled with a second Region (perhaps "Un-Flooded House"). Or you could modify
the game so flipping that switch checks a regular AP location in addition to lowering the water level.
Events are never required, but it may be cleaner to use an event if e.g. flipping that switch affects the logic in
dozens of half-flooded areas that would all otherwise need additional Regions, and you don't want it to be a regular
location. It depends on the game.
## Implementation
### Your World
@@ -488,8 +520,8 @@ In addition, the following methods can be implemented and are called in this ord
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
* `create_items(self)`
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
after this step. Locations cannot be moved to different regions after this step.
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions after
this step. Locations cannot be moved to different regions after this step. This includes event items and locations.
* `set_rules(self)`
called to set access and item rules on locations and entrances.
* `connect_entrances(self)`
@@ -501,7 +533,7 @@ In addition, the following methods can be implemented and are called in this ord
called to modify item placement before, during, and after the regular fill process; all finishing before
`generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
are any items that need to be filled this way, but need to be in state while you fill other items, they can be
returned from `get_prefill_items`.
returned from `get_pre_fill_items`.
* `generate_output(self, output_directory: str)`
creates the output files if there is output to be generated. When this is called,
`self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the

View File

@@ -138,11 +138,6 @@ Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";

178
kvui.py
View File

@@ -60,7 +60,10 @@ from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupporting
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
from kivymd.uix.screen import MDScreen
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.menu.menu import MDDropdownTextItem
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
@@ -726,6 +729,10 @@ class MessageBox(Popup):
self.height += max(0, label.height - 18)
class MDNavigationItemBase(MDNavigationItem):
text = StringProperty(None)
class ButtonsPrompt(MDDialog):
def __init__(self, title: str, text: str, response: typing.Callable[[str], None],
*prompts: str, **kwargs) -> None:
@@ -766,58 +773,34 @@ class ButtonsPrompt(MDDialog):
)
class ClientTabs(MDTabsSecondary):
carousel: MDTabsCarousel
lock_swiping = True
class MDScreenManagerBase(MDScreenManager):
current_tab: MDNavigationItemBase
local_screen_names: list[str]
def __init__(self, *args, **kwargs):
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
self.size_hint_y = 1
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.local_screen_names = []
def _check_panel_height(self, *args):
self.ids.tab_scroll.height = dp(38)
def update_indicator(
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
) -> None:
def update_indicator(*args):
indicator_pos = (0, 0)
indicator_size = (0, 0)
item_text_object = self._get_tab_item_text_icon_object()
if item_text_object:
indicator_pos = (
instance.x + dp(12),
self.indicator.pos[1]
if not self._tabs_carousel
else self._tabs_carousel.height,
)
indicator_size = (
instance.width - dp(24),
self.indicator_height,
)
Animation(
pos=indicator_pos,
size=indicator_size,
d=0 if not self.indicator_anim else self.indicator_duration,
t=self.indicator_transition,
).start(self.indicator)
if not instance:
self.indicator.pos = (x, self.indicator.pos[1])
self.indicator.size = (w, self.indicator_height)
def add_widget(self, widget: Widget, *args, **kwargs) -> None:
super().add_widget(widget, *args, **kwargs)
if "index" in kwargs:
self.local_screen_names.insert(kwargs["index"], widget.name)
else:
Clock.schedule_once(update_indicator)
self.local_screen_names.append(widget.name)
def remove_tab(self, tab, content=None):
if content is None:
content = tab.content
self.ids.container.remove_widget(tab)
self.carousel.remove_widget(content)
self.on_size(self, self.size)
def switch_screens(self, new_tab: MDNavigationItemBase) -> None:
"""
Called whenever the user clicks a tab to switch to a different screen.
:param new_tab: The new screen to switch to's tab.
"""
name = new_tab.text
if self.local_screen_names.index(name) > self.local_screen_names.index(self.current_screen.name):
self.transition.direction = "left"
else:
self.transition.direction = "right"
self.current = name
self.current_tab = new_tab
class CommandButton(MDButton, MDTooltip):
@@ -845,6 +828,9 @@ class GameManager(ThemedApp):
main_area_container: MDGridLayout
""" subclasses can add more columns beside the tabs """
tabs: MDNavigationBar
screens: MDScreenManagerBase
def __init__(self, ctx: context_type):
self.title = self.base_title
self.ctx = ctx
@@ -874,7 +860,7 @@ class GameManager(ThemedApp):
@property
def tab_count(self):
if hasattr(self, "tabs"):
return max(1, len(self.tabs.tab_list))
return max(1, len(self.tabs.children))
return 1
def on_start(self):
@@ -914,30 +900,32 @@ class GameManager(ThemedApp):
self.grid.add_widget(self.progressbar)
# middle part
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in
self.logging_pairs))
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
self.screens = MDScreenManagerBase(pos_hint={"center_x": 0.5})
self.tabs = MDNavigationBar(orientation="horizontal", size_hint_y=None, height=dp(40), set_bars_color=True)
# bind the method to the bar for back compatibility
self.tabs.remove_tab = self.remove_client_tab
self.screens.current_tab = self.add_client_tab(
"All" if len(self.logging_pairs) > 1 else "Archipelago",
UILog(*(logging.getLogger(logger_name) for logger_name, name in self.logging_pairs)),
)
self.log_panels["All"] = self.screens.current_tab.content
self.screens.current_tab.active = True
for logger_name, display_name in self.logging_pairs:
bridge_logger = logging.getLogger(logger_name)
self.log_panels[display_name] = UILog(bridge_logger)
if len(self.logging_pairs) > 1:
panel = MDTabsItem(MDTabsItemText(text=display_name))
panel.content = self.log_panels[display_name]
# show Archipelago tab if other logging is present
self.tabs.carousel.add_widget(panel.content)
self.tabs.add_widget(panel)
self.add_client_tab(display_name, self.log_panels[display_name])
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
hint_panel = self.add_client_tab("Hints", HintLayout(self.hint_log))
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
self.main_area_container.add_widget(self.tabs)
tab_container = MDGridLayout(size_hint_y=1, cols=1)
tab_container.add_widget(self.tabs)
tab_container.add_widget(self.screens)
self.main_area_container.add_widget(tab_container)
self.grid.add_widget(self.main_area_container)
@@ -974,25 +962,61 @@ class GameManager(ThemedApp):
return self.container
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content."""
new_tab = MDTabsItem(MDTabsItemText(text=title))
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> MDNavigationItemBase:
"""
Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content.
:param title: The title of the tab.
:param content: The Widget to be added as content for this tab's new MDScreen. Will also be added to the
returned tab as tab.content.
:param index: The index to insert the tab at. Defaults to -1, meaning the tab will be appended to the end.
:return: The new tab.
"""
if self.tabs.children:
self.tabs.add_widget(MDDivider(orientation="vertical"))
new_tab = MDNavigationItemBase(text=title)
new_tab.content = content
if -1 < index <= len(self.tabs.carousel.slides):
new_tab.bind(on_release=self.tabs.set_active_item)
new_tab._tabs = self.tabs
self.tabs.ids.container.add_widget(new_tab, index=index)
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
new_screen = MDScreen(name=title)
new_screen.add_widget(content)
if -1 < index <= len(self.tabs.children):
remapped_index = len(self.tabs.children) - index
self.tabs.add_widget(new_tab, index=remapped_index)
self.screens.add_widget(new_screen, index=index)
else:
self.tabs.add_widget(new_tab)
self.tabs.carousel.add_widget(new_tab.content)
self.screens.add_widget(new_screen)
return new_tab
def remove_client_tab(self, tab: MDNavigationItemBase) -> None:
"""
Called to remove a tab and its screen.
:param tab: The tab to remove.
"""
tab_index = self.tabs.children.index(tab)
# if the tab is currently active we need to swap before removing it
if tab == self.screens.current_tab:
if not tab_index:
# account for the divider
swap_index = tab_index + 2
else:
swap_index = tab_index - 2
self.tabs.children[swap_index].on_release()
# self.screens.switch_screens(self.tabs.children[swap_index])
# get the divider to the left if we can
if not tab_index:
divider_index = tab_index + 1
else:
divider_index = tab_index - 1
self.tabs.remove_widget(self.tabs.children[divider_index])
self.tabs.remove_widget(tab)
self.screens.remove_widget(self.screens.get_screen(tab.text))
def update_texts(self, dt):
for slide in self.tabs.carousel.slides:
if hasattr(slide, "fix_heights"):
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
if hasattr(self.screens.current_tab.content, "fix_heights"):
getattr(self.screens.current_tab.content, "fix_heights")()
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \

View File

@@ -382,7 +382,7 @@ class World(metaclass=AutoWorldRegister):
def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_pre_fill_items`.
"""
pass

View File

@@ -221,9 +221,6 @@ components: List[Component] = [
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Minecraft
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
file_identifier=SuffixIdentifier('.apmc')),
# Ocarina of Time
Component('OoT Client', 'OoTClient',
file_identifier=SuffixIdentifier('.apz5')),
@@ -246,6 +243,5 @@ components: List[Component] = [
# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used
icon_paths = {
'icon': local_path('data', 'icon.png'),
'mcicon': local_path('data', 'mcicon.png'),
'discord': local_path('data', 'discord-mark-blue.png'),
}

View File

@@ -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"

View File

@@ -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:

View File

@@ -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)

View File

@@ -20,16 +20,17 @@ A short period after receiving an item, you will get a notification indicating y
## FAQs
- Do I need the DLC to play this?
- Yes, you need both Rise & Fall and Gathering Storm.
- You need both expansions, Rise & Fall and Gathering Storm. You do not need the other DLCs but they fully work with this.
- Does this work with Multiplayer?
- It does not and, despite my best efforts, probably won't until there's a new way for external programs to be able to interact with the game.
- Does my mod that reskins Barbarians as various Pro Wrestlers work with this?
- Only one way to find out! Any mods that modify techs/civics will most likely cause issues, though.
- Does this work with other mods?
- A lot of mods seem to work without issues combined with this, but you should avoid any mods that change things in the tech or civic tree, as even if they would work it could cause issues with the logic.
- "Help! I can't see any of the items that have been sent to me!"
- Both trees by default will show you the researchable Archipelago locations. To view the normal tree, you can click "Toggle Archipelago Tree" in the top-left corner of the tree view.
- "Oh no! I received the Machinery tech and now instead of getting an Archer next turn, I have to wait an additional 10 turns to get a Crossbowman!"
- Vanilla prevents you from building units of the same class from an earlier tech level after you have researched a later variant. For example, this could be problematic if someone unlocks Crossbowmen for you right out the gate since you won't be able to make Archers (which have a much lower production cost).
Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
- Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
- If you think you should be able to make Field Cannons but seemingly can't try disabling `Telecommunications`
- "How does DeathLink work? Am I going to have to start a new game every time one of my friends dies?"
- Heavens no, my fellow Archipelago appreciator. When configuring your Archipelago options for Civilization on the options page, there are several choices available for you to fine tune the way you'd like to be punished for the follies of your friends. These include: Having a random unit destroyed, losing a percentage of gold or faith, or even losing a point on your era score. If you can't make up your mind, you can elect to have any of them be selected every time a death link is sent your way.
In the event you lose one of your units in combat (this means captured units don't count), then you will send a death link event to the rest of your friends.
@@ -39,7 +40,8 @@ Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to
1. `TECH_WRITING`
2. `TECH_EDUCATION`
3. `TECH_CHEMISTRY`
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.json).
- An important thing to note is that the seaport is part of progressive industrial zones, due to electricity having both an industrial zone building and the seaport.
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.py).
## Boostsanity
Boostsanity takes all of the Eureka & Inspiration events and makes them location checks. This feature is the one to change up the way Civilization is played in an AP multiworld/randomizer. What normally are mundane tasks that are passively collected now become a novel and interesting bucket list that you need to pay attention to in order to unlock items for yourself and others!
@@ -56,4 +58,3 @@ Boosts have logic associated with them in order to verify you can always reach t
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
- There's too many boosts, how will I know which one's I should focus on?!
- In order to give a little more focus to all the boosts rather than just arbitrarily picking them at random, items in both of the vanilla trees will now have an advisor icon on them if its associated boost contains a progression item.

View File

@@ -6,12 +6,14 @@ This guide is meant to help you get up and running with Civilization VI in Archi
The following are required in order to play Civ VI in Archipelago:
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux)
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux).
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) v0.4.5 or higher.
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- The latest version of the [Civ VI AP Mod](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
## Enabling the tuner
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
@@ -20,27 +22,32 @@ In the main menu, navigate to the "Game Options" page. On the "Game" menu, make
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
5. Your finished mod folder should look something like this:
- Civ VI Mods Directory
- civilization_archipelago_mod
- NewItems.xml
- InitOptions.lua
- Archipelago.modinfo
- All the other mod files, etc.
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
## Configuring your game
When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
## Troubleshooting
- If you have troubles with file extension related stuff, make sure Windows shows file extensions as they are turned off by default. If you don't know how to turn them on it is just a quick google search away.
- If you are getting an error: "The remote computer refused the network connection", or something else related to the client (or tuner) not being able to connect, it likely indicates the tuner is not actually enabled. One simple way to verify that it is enabled is, after completing the setup steps, go to Main Menu &rarr; Options &rarr; Look for an option named "Tuner" and verify it is set to "Enabled"
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are.
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are. This can resend certain items to you, like one time bonuses.
- If the archipelago mod does not appear in the mod selector in the game, make sure the mod is correctly placed as a folder in the `Sid Meier's Civilization VI\Mods` folder, there should not be any loose files in there only folders. As in the path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
- If it still does not appear make sure you have the right folder, one way to verify you are in the right place is to find the general folder area where your Civ VI save files are located.
- If you get an error when trying to start a game saying `Error - One or more Mods failed to load content`, make sure the files from the `.apcivvi` are placed into the `civilization_archipelago_mod` as loose files and not as a folder.
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.

View File

@@ -734,8 +734,8 @@ def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bo
magic_items_array[array_offset] += 1
# Add the start inventory arrays to the offset data in bytes form.
start_inventory_data[0x680080] = bytes(magic_items_array)
start_inventory_data[0x6800A0] = bytes(cards_array)
start_inventory_data[0x690080] = bytes(magic_items_array)
start_inventory_data[0x6900A0] = bytes(cards_array)
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
# possible Max Ups.

View File

@@ -132,40 +132,40 @@ start_inventory_giver = [
# Magic Items
0x13, 0x48, # ldr r0, =0x202572F
0x14, 0x49, # ldr r1, =0x8680080
0x14, 0x49, # ldr r1, =0x8690080
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x08, 0x2A, # cmp r2, #8
0xFA, 0xDB, # blt 0x8680006
0xFA, 0xDB, # blt 0x8690006
# Max Ups
0x11, 0x48, # ldr r0, =0x202572C
0x12, 0x49, # ldr r1, =0x8680090
0x12, 0x49, # ldr r1, =0x8690090
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x03, 0x2A, # cmp r2, #3
0xFA, 0xDB, # blt 0x8680016
0xFA, 0xDB, # blt 0x8690016
# Cards
0x0F, 0x48, # ldr r0, =0x2025674
0x10, 0x49, # ldr r1, =0x86800A0
0x10, 0x49, # ldr r1, =0x86900A0
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x14, 0x2A, # cmp r2, #0x14
0xFA, 0xDB, # blt 0x8680026
0xFA, 0xDB, # blt 0x8690026
# Inventory Items (not currently supported)
0x0D, 0x48, # ldr r0, =0x20256ED
0x0E, 0x49, # ldr r1, =0x86800C0
0x0E, 0x49, # ldr r1, =0x86900C0
0x00, 0x22, # mov r2, #0
0x8B, 0x5C, # ldrb r3, [r1, r2]
0x83, 0x54, # strb r3, [r0, r2]
0x01, 0x32, # adds r2, #1
0x36, 0x2A, # cmp r2, #36
0xFA, 0xDB, # blt 0x8680036
0xFA, 0xDB, # blt 0x8690036
# Return to the function that checks for Magician Mode.
0xBA, 0x21, # movs r1, #0xBA
0x89, 0x00, # lsls r1, r1, #2
@@ -176,13 +176,13 @@ start_inventory_giver = [
# LDR number pool
0x78, 0x7F, 0x00, 0x08,
0x2F, 0x57, 0x02, 0x02,
0x80, 0x00, 0x68, 0x08,
0x80, 0x00, 0x69, 0x08,
0x2C, 0x57, 0x02, 0x02,
0x90, 0x00, 0x68, 0x08,
0x90, 0x00, 0x69, 0x08,
0x74, 0x56, 0x02, 0x02,
0xA0, 0x00, 0x68, 0x08,
0xA0, 0x00, 0x69, 0x08,
0xED, 0x56, 0x02, 0x02,
0xC0, 0x00, 0x68, 0x08,
0xC0, 0x00, 0x69, 0x08,
]
max_max_up_checker = [

View File

@@ -335,8 +335,8 @@ class CVCotMPatchExtensions(APPatchExtension):
rom_data.write_bytes(0x679A60, patches.kickless_roc_height_shortener)
# Give the player their Start Inventory upon entering their name on a new file.
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x68, 0x08])
rom_data.write_bytes(0x680000, patches.start_inventory_giver)
rom_data.write_bytes(0x7F70, [0x00, 0x48, 0x87, 0x46, 0x00, 0x00, 0x69, 0x08])
rom_data.write_bytes(0x690000, patches.start_inventory_giver)
# Prevent Max Ups from exceeding 255.
rom_data.write_bytes(0x5E170, [0x00, 0x4A, 0x97, 0x46, 0x00, 0x00, 0x6A, 0x08])

View File

@@ -75,6 +75,13 @@ class DarkSouls3World(World):
"""The pool of all items within this particular world. This is a subset of
`self.multiworld.itempool`."""
missable_dupe_prog_locs: Set[str] = {"PC: Storm Ruler - Siegward",
"US: Pyromancy Flame - Cornyx",
"US: Tower Key - kill Irina"}
"""Locations whose vanilla item is a missable duplicate of a non-missable progression item.
If vanilla, these locations shouldn't be expected progression, so they aren't created and don't get rules.
"""
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.all_excluded_locations = set()
@@ -258,10 +265,7 @@ class DarkSouls3World(World):
new_location.progress_type = LocationProgressType.EXCLUDED
else:
# Don't allow missable duplicates of progression items to be expected progression.
if location.name in {"PC: Storm Ruler - Siegward",
"US: Pyromancy Flame - Cornyx",
"US: Tower Key - kill Irina"}:
continue
if location.name in self.missable_dupe_prog_locs: continue
# Replace non-randomized items with events that give the default item
event_item = (
@@ -1286,8 +1290,9 @@ class DarkSouls3World(World):
data = location_dictionary[location]
if data.dlc and not self.options.enable_dlc: continue
if data.ngp and not self.options.enable_ngp: continue
# Don't add rules to missable duplicates of progression items
if location in self.missable_dupe_prog_locs and not self._is_location_available(location): continue
if not self._is_location_available(location): continue
if isinstance(rule, str):
assert item_dictionary[rule].classification == ItemClassification.progression
rule = lambda state, item=rule: state.has(item, self.player)

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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
}
]
},

View File

@@ -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)

View File

@@ -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

View File

@@ -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!

View File

@@ -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.")

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'))

View File

@@ -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"
]
}

View File

@@ -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
}
}

View File

@@ -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"
]
}
}

View File

@@ -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"]
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)``<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

View File

@@ -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.

View File

@@ -1 +0,0 @@
requests >= 2.28.1 # used by client

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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']],
])

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -23,6 +23,7 @@ DATA_LOCATIONS = {
"DexSanityFlag": (0x1A71, 19),
"GameStatus": (0x1A84, 0x01),
"Money": (0x141F, 3),
"CurrentMap": (0x1436, 1),
"ResetCheck": (0x0100, 4),
# First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
# First should never be above 0x0F. This is just before Event Flags.
@@ -65,6 +66,7 @@ class PokemonRBClient(BizHawkClient):
self.banking_command = None
self.game_state = False
self.last_death_link = 0
self.current_map = 0
async def validate_rom(self, ctx):
game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
@@ -230,6 +232,10 @@ class PokemonRBClient(BizHawkClient):
}])
self.banking_command = None
if data["CurrentMap"][0] != self.current_map:
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
self.current_map = data["CurrentMap"][0]
# VICTORY
if data["EventFlag"][280] & 1 and not ctx.finished_game:

View File

@@ -15,7 +15,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Optional Software
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Configuring BizHawk
@@ -109,7 +109,7 @@ server uses password, type in the bottom textfield `/connect <address>:<port> [p
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/palex00/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Open PopTracker, and load the Pokémon Red and Blue pack.
3. Click on the "AP" symbol at the top.
4. Enter the AP address, slot name and password.

View File

@@ -16,7 +16,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
## Software Opcional
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Configurando BizHawk
@@ -114,7 +114,7 @@ presiona enter (si el servidor usa contraseña, escribe en el campo de texto inf
Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/palex00/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
3. Haz clic en el símbolo "AP" en la parte superior.
4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).

View File

@@ -1,6 +1,7 @@
import typing
from BaseClasses import MultiWorld
from Options import OptionError
from worlds.AutoWorld import World
from .Names import LocationName
@@ -99,8 +100,9 @@ def get_gate_bosses(world: World):
pass
if boss in plando_bosses:
# TODO: Raise error here. Duplicates not allowed
pass
raise OptionError(f"Invalid input for option `plando_bosses`: "
f"No Duplicate Bosses permitted ({boss}) - for "
f"{world.player_name}")
plando_bosses[boss_num] = boss
@@ -108,13 +110,14 @@ def get_gate_bosses(world: World):
available_bosses.remove(boss)
for x in range(world.options.number_of_level_gates):
if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
available_bosses.extend(gate_bosses_with_requirements_table)
if (10 not in selected_bosses) and (king_boom_boo not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
available_bosses.extend(gate_bosses_with_requirements_table.keys())
world.random.shuffle(available_bosses)
chosen_boss = available_bosses[0]
if plando_bosses[x] != "None":
available_bosses.append(plando_bosses[x])
if plando_bosses[x] not in available_bosses:
available_bosses.append(plando_bosses[x])
chosen_boss = plando_bosses[x]
selected_bosses.append(all_gate_bosses_table[chosen_boss])

View File

@@ -324,7 +324,8 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
add_rule_safe(multiworld, LocationName.iron_gate_5, player,
lambda state: state.has(ItemName.eggman_large_cannon, player))
add_rule_safe(multiworld, LocationName.dry_lagoon_5, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_treasure_scope, player))
add_rule_safe(multiworld, LocationName.sand_ocean_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule_safe(multiworld, LocationName.egg_quarters_5, player,
@@ -407,8 +408,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
@@ -1402,8 +1402,6 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
state.has(ItemName.eggman_large_cannon, player)))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_treasure_scope, player)))
@@ -1724,6 +1722,9 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.white_jungle_itembox_8, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
add_rule(multiworld.get_location(LocationName.sky_rail_itembox_8, player),
lambda state: (state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_mystic_melody, player)))
add_rule(multiworld.get_location(LocationName.mad_space_itembox_8, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_itembox_8, player),
@@ -2308,8 +2309,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
@@ -2980,8 +2980,6 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
state.has(ItemName.eggman_jet_engine, player)))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: (state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_treasure_scope, player)))
@@ -3593,8 +3591,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
@@ -3643,9 +3640,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player),
lambda state: state.has(ItemName.tails_bazooka, player))
add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player),
@@ -3771,10 +3765,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Animal Upgrade Requirements
if world.options.animalsanity:
add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player),
@@ -3839,8 +3829,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
lambda state: state.has(ItemName.rouge_iron_boots, player))
add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
@@ -3976,8 +3965,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
state.has(ItemName.tails_bazooka, player))
add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player),
lambda state: state.has(ItemName.sonic_flame_ring, player))
add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
@@ -4035,8 +4022,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))
add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player),
lambda state: state.has(ItemName.rouge_treasure_scope, player))

View File

@@ -12,7 +12,7 @@ from .Constants import *
def launch_client(*args: str):
from .Client import launch
launch_subprocess(launch(*args), name=CLIENT_NAME)
launch_subprocess(launch, name=CLIENT_NAME, args=args)
components.append(

View File

@@ -57,7 +57,7 @@ Ein Pop-Up erscheint, das das/die erhaltene(n) Item(s) und eventuell weitere Inf
Hier ist ein Spicker für die Englischarbeit (bloß nicht dem Lehrer zeigen):
![image](https://raw.githubusercontent.com/BlastSlimey/Archipelago/refs/heads/main/worlds/shapez/docs/shapesanity_full.png)
![image](/static/generated/docs/shapez/shapesanity_full.png)
## Kann ich auch weitere Mods neben dem AP Client installieren?

View File

@@ -56,7 +56,7 @@ A pop-up will show, which item(s) were received, with additional information on
Here's a cheat sheet:
![image](https://raw.githubusercontent.com/BlastSlimey/Archipelago/refs/heads/main/worlds/shapez/docs/shapesanity_full.png)
![image](/static/generated/docs/shapez/shapesanity_full.png)
## Can I use other mods alongside the AP client?

View File

@@ -261,13 +261,13 @@ class ShiversWorld(World):
data.type == ItemType.POT_DUPLICATE]
elif self.options.full_pots == "complete":
return [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
else:
pool = []
pieces = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_DUPLICATE]
complete = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
for i in range(10):
if self.pot_completed_list[i] == 0:
pool.append(pieces[i])

View File

@@ -271,11 +271,11 @@ solar_essence = BundleItem(Loot.solar_essence)
void_essence = BundleItem(Loot.void_essence)
petrified_slime = BundleItem(Mineral.petrified_slime)
blue_slime_egg = BundleItem(Loot.blue_slime_egg)
red_slime_egg = BundleItem(Loot.red_slime_egg)
purple_slime_egg = BundleItem(Loot.purple_slime_egg)
green_slime_egg = BundleItem(Loot.green_slime_egg)
tiger_slime_egg = BundleItem(Loot.tiger_slime_egg, source=BundleItem.Sources.island)
blue_slime_egg = BundleItem(AnimalProduct.slime_egg_blue)
red_slime_egg = BundleItem(AnimalProduct.slime_egg_red)
purple_slime_egg = BundleItem(AnimalProduct.slime_egg_purple)
green_slime_egg = BundleItem(AnimalProduct.slime_egg_green)
tiger_slime_egg = BundleItem(AnimalProduct.slime_egg_tiger, source=BundleItem.Sources.island)
cherry_bomb = BundleItem(Bomb.cherry_bomb, 5)
bomb = BundleItem(Bomb.bomb, 2)

View File

@@ -386,7 +386,7 @@ coppper_slot_machine = skill_recipe(ModMachine.copper_slot_machine, ModSkill.luc
Forageable.salmonberry: 1, Material.clay: 1, Trash.joja_cola: 1}, ModNames.luck_skill)
gold_slot_machine = skill_recipe(ModMachine.gold_slot_machine, ModSkill.luck, 4, {MetalBar.gold: 15, ModMachine.copper_slot_machine: 1}, ModNames.luck_skill)
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 4, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 4, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
iridium_slot_machine = skill_recipe(ModMachine.iridium_slot_machine, ModSkill.luck, 6, {MetalBar.iridium: 15, ModMachine.gold_slot_machine: 1}, ModNames.luck_skill)
radioactive_slot_machine = skill_recipe(ModMachine.radioactive_slot_machine, ModSkill.luck, 8, {MetalBar.radioactive: 15, ModMachine.iridium_slot_machine: 1}, ModNames.luck_skill)
all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes}

View File

@@ -6,7 +6,7 @@ id,name,classification,groups,mod_name
18,Greenhouse,progression,COMMUNITY_REWARD,
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
20,Minecarts Repair,useful,COMMUNITY_REWARD,
21,Bus Repair,progression,COMMUNITY_REWARD,
21,Bus Repair,progression,"COMMUNITY_REWARD,DESERT_TRANSPORTATION",
22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD,
23,Stardrop,progression,,
24,Progressive Backpack,progression,,
@@ -63,8 +63,8 @@ id,name,classification,groups,mod_name
77,Combat Level,progression,SKILL_LEVEL_UP,
78,Earth Obelisk,progression,WIZARD_BUILDING,
79,Water Obelisk,progression,WIZARD_BUILDING,
80,Desert Obelisk,progression,WIZARD_BUILDING,
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND",
80,Desert Obelisk,progression,"WIZARD_BUILDING,DESERT_TRANSPORTATION",
81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND,ISLAND_TRANSPORTATION",
82,Junimo Hut,useful,WIZARD_BUILDING,
83,Gold Clock,progression,WIZARD_BUILDING,
84,Progressive Coop,progression,BUILDING,
@@ -242,7 +242,7 @@ id,name,classification,groups,mod_name
257,Peach Sapling,progression,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
258,Banana Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
259,Mango Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
260,Boat Repair,progression,GINGER_ISLAND,
260,Boat Repair,progression,"GINGER_ISLAND,ISLAND_TRANSPORTATION",
261,Open Professor Snail Cave,progression,GINGER_ISLAND,
262,Island North Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
263,Island West Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
1 id name classification groups mod_name
6 18 Greenhouse progression COMMUNITY_REWARD
7 19 Glittering Boulder Removed progression COMMUNITY_REWARD
8 20 Minecarts Repair useful COMMUNITY_REWARD
9 21 Bus Repair progression COMMUNITY_REWARD COMMUNITY_REWARD,DESERT_TRANSPORTATION
10 22 Progressive Movie Theater progression,trap COMMUNITY_REWARD
11 23 Stardrop progression
12 24 Progressive Backpack progression
63 77 Combat Level progression SKILL_LEVEL_UP
64 78 Earth Obelisk progression WIZARD_BUILDING
65 79 Water Obelisk progression WIZARD_BUILDING
66 80 Desert Obelisk progression WIZARD_BUILDING WIZARD_BUILDING,DESERT_TRANSPORTATION
67 81 Island Obelisk progression WIZARD_BUILDING,GINGER_ISLAND WIZARD_BUILDING,GINGER_ISLAND,ISLAND_TRANSPORTATION
68 82 Junimo Hut useful WIZARD_BUILDING
69 83 Gold Clock progression WIZARD_BUILDING
70 84 Progressive Coop progression BUILDING
242 257 Peach Sapling progression RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
243 258 Banana Sapling progression GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
244 259 Mango Sapling progression GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY
245 260 Boat Repair progression GINGER_ISLAND GINGER_ISLAND,ISLAND_TRANSPORTATION
246 261 Open Professor Snail Cave progression GINGER_ISLAND
247 262 Island North Turtle progression GINGER_ISLAND,WALNUT_PURCHASE
248 263 Island West Turtle progression GINGER_ISLAND,WALNUT_PURCHASE

View File

@@ -33,6 +33,8 @@ class Group(enum.Enum):
SKILL_MASTERY = enum.auto()
BUILDING = enum.auto()
WIZARD_BUILDING = enum.auto()
DESERT_TRANSPORTATION = enum.auto()
ISLAND_TRANSPORTATION = enum.auto()
ARCADE_MACHINE_BUFFS = enum.auto()
BASE_RESOURCE = enum.auto()
WARP_TOTEM = enum.auto()

View File

@@ -168,15 +168,16 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.slime_egg_tiger: self.can_fish_pond(Fish.lionfish, *(Forageable.ginger, Fruit.pineapple, Fruit.mango)) & self.time.has_lived_months(12) &
self.building.has_building(Building.slime_hutch) & self.monster.can_kill(Monster.tiger_slime),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet...
AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
@@ -233,7 +234,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), #
Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton),
Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe) & self.received("Open Professor Snail Cave"),
Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
Fossil.fossilized_spine: self.fishing.can_fish_at(Region.dig_site),
Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper),
@@ -288,9 +289,9 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
MetalBar.radioactive: self.can_smelt(Ore.radioactive),
Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
Ore.iridium: self.count(2, *(self.mine.can_mine_in_the_skull_cavern(), self.can_fish_pond(Fish.super_cucumber), self.tool.has_tool(Tool.pan, ToolMaterial.iridium))),
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
@@ -381,5 +382,8 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
def can_use_obelisk(self, obelisk: str) -> StardewRule:
return self.region.can_reach(Region.farm) & self.received(obelisk)
def can_fish_pond(self, fish: str) -> StardewRule:
return self.building.has_building(Building.fish_pond) & self.has(fish)
def can_fish_pond(self, fish: str, *items: str) -> StardewRule:
rule = self.building.has_building(Building.fish_pond) & self.has(fish)
if items:
rule = rule & self.has_all(*items)
return rule

View File

@@ -1,9 +1,4 @@
class Loot:
blue_slime_egg = "Blue Slime Egg"
red_slime_egg = "Red Slime Egg"
purple_slime_egg = "Purple Slime Egg"
green_slime_egg = "Green Slime Egg"
tiger_slime_egg = "Tiger Slime Egg"
slime = "Slime"
bug_meat = "Bug Meat"
bat_wing = "Bat Wing"

View File

@@ -11,7 +11,7 @@ class EntranceRandomizationAssertMixin:
non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag]
for non_progression_connections in non_progression_connections:
with self.subTest(connection=non_progression_connections):
with self.subTest(connection=non_progression_connections.name):
self.assert_can_reach_entrance(non_progression_connections.name)

View File

@@ -12,14 +12,14 @@ from ...regions.regions import create_all_regions, create_all_connections
class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_with_ginger_island_by_name.values():
with self.subTest(region=region):
with self.subTest(region=region.name):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_with_ginger_island_by_name.values():
with self.subTest(connection=connection):
with self.subTest(connection=connection.name):
self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")
@@ -27,14 +27,14 @@ class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_without_ginger_island_by_name.values():
with self.subTest(region=region):
with self.subTest(region=region.name):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_without_ginger_island_by_name.values():
with self.subTest(connection=connection):
with self.subTest(connection=connection.name):
self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")

View File

@@ -8,7 +8,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
SkillProgression.internal_name: SkillProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_progressive,
Fishsanity.internal_name: Fishsanity.option_all,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
@@ -18,7 +18,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
fish_and_items = {
Fish.crimsonfish: ["Beach Bridge"],
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
Fish.woodskip: ["Progressive Axe", "Progressive Axe", "Progressive Weapon"], # For the ores to get the axe upgrades
Fish.mutant_carp: ["Rusty Key"],
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
Fish.lionfish: ["Boat Repair"],
@@ -26,8 +26,8 @@ class TestNeedRegionToCatchFish(SVTestBase):
Fish.stingray: ["Boat Repair", "Island Resort"],
Fish.ghostfish: ["Progressive Weapon"],
Fish.stonefish: ["Progressive Weapon"],
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe"],
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe", "Progressive Pickaxe"],
Fish.sandfish: ["Bus Repair"],
Fish.scorpion_carp: ["Desert Obelisk"],
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
@@ -37,6 +37,7 @@ class TestNeedRegionToCatchFish(SVTestBase):
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
}
self.collect("Progressive Fishing Rod", 4)
self.original_state = self.multiworld.state.copy()
for fish in fish_and_items:
with self.subTest(f"Region rules for {fish}"):

View File

@@ -1,61 +1,122 @@
from typing import Any
tww_options_presets: dict[str, dict[str, Any]] = {
"Tournament S7": {
"Tournament S8": {
"progression_dungeon_secrets": True,
"progression_combat_secret_caves": True,
"progression_short_sidequests": True,
"progression_long_sidequests": True,
"progression_spoils_trading": True,
"progression_big_octos_gunboats": True,
"progression_mail": True,
"progression_platforms_rafts": True,
"progression_submarines": True,
"progression_big_octos_gunboats": True,
"progression_expensive_purchases": True,
"progression_island_puzzles": True,
"progression_misc": True,
"randomize_mapcompass": "startwith",
"randomize_bigkeys": "startwith",
"required_bosses": True,
"num_required_bosses": 3,
"num_required_bosses": 4,
"included_dungeons": ["Forsaken Fortress"],
"chest_type_matches_contents": True,
"logic_obscurity": "hard",
"randomize_dungeon_entrances": True,
"randomize_starting_island": True,
"add_shortcut_warps_between_dungeons": True,
"start_inventory_from_pool": {
"Telescope": 1,
"Wind Waker": 1,
"Goddess Tingle Statue": 1,
"Earth Tingle Statue": 1,
"Wind Tingle Statue": 1,
"Wind's Requiem": 1,
"Ballad of Gales": 1,
"Command Melody": 1,
"Earth God's Lyric": 1,
"Wind God's Aria": 1,
"Song of Passing": 1,
"Progressive Magic Meter": 2,
"Triforce Shard 1": 1,
"Triforce Shard 2": 1,
"Triforce Shard 3": 1,
"Skull Necklace": 20,
"Golden Feather": 20,
"Knight's Crest": 10,
"Green Chu Jelly": 15,
"Nayru's Pearl": 1,
"Din's Pearl": 1,
},
"start_location_hints": ["Ganon's Tower - Maze Chest"],
"start_location_hints": [
"Windfall Island - Chu Jelly Juice Shop - Give 15 Blue Chu Jelly",
"Ganon's Tower - Maze Chest",
],
"exclude_locations": [
"Outset Island - Orca - Give 10 Knight's Crests",
"Outset Island - Great Fairy",
"Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly",
"Windfall Island - Mrs. Marie - Give 1 Joy Pendant",
"Windfall Island - Mrs. Marie - Give 21 Joy Pendants",
"Windfall Island - Mrs. Marie - Give 40 Joy Pendants",
"Windfall Island - Maggie's Father - Give 20 Skull Necklaces",
"Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers",
"Windfall Island - Lenzo's House - Become Lenzo's Assistant",
"Windfall Island - Lenzo's House - Bring Forest Firefly",
"Windfall Island - Sam - Decorate the Town",
"Windfall Island - Kamo - Full Moon Photo",
"Windfall Island - Linda and Anton",
"Dragon Roost Island - Secret Cave",
"Greatfish Isle - Hidden Chest",
"Mother and Child Isles - Inside Mother Isle",
"Fire Mountain - Cave - Chest",
"Fire Mountain - Lookout Platform Chest",
"Fire Mountain - Lookout Platform - Destroy the Cannons",
"Fire Mountain - Big Octo",
"Mailbox - Letter from Hoskit's Girlfriend",
"Headstone Island - Top of the Island",
"Headstone Island - Submarine",
"Earth Temple - Behind Curtain Next to Hammer Button",
"The Great Sea - Goron Trading Reward",
"The Great Sea - Withered Trees",
"Private Oasis - Big Octo",
"Boating Course - Raft",
"Boating Course - Cave",
"Stone Watcher Island - Cave",
"Stone Watcher Island - Lookout Platform Chest",
"Stone Watcher Island - Lookout Platform - Destroy the Cannons",
"Overlook Island - Cave",
"Bird's Peak Rock - Cave",
"Pawprint Isle - Wizzrobe Cave",
"Thorned Fairy Island - Great Fairy",
"Thorned Fairy Island - Northeastern Lookout Platform - Destroy the Cannons",
"Thorned Fairy Island - Southwestern Lookout Platform - Defeat the Enemies",
"Eastern Fairy Island - Great Fairy",
"Eastern Fairy Island - Lookout Platform - Defeat the Cannons and Enemies",
"Western Fairy Island - Great Fairy",
"Southern Fairy Island - Great Fairy",
"Northern Fairy Island - Great Fairy",
"Western Fairy Island - Lookout Platform",
"Tingle Island - Ankle - Reward for All Tingle Statues",
"Tingle Island - Big Octo",
"Diamond Steppe Island - Big Octo",
"Rock Spire Isle - Cave",
"Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item",
"Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item",
"Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item",
"Rock Spire Isle - Western Lookout Platform - Destroy the Cannons",
"Rock Spire Isle - Eastern Lookout Platform - Destroy the Cannons",
"Rock Spire Isle - Center Lookout Platform",
"Rock Spire Isle - Southeast Gunboat",
"Shark Island - Cave",
"Horseshoe Island - Northwestern Lookout Platform",
"Horseshoe Island - Southeastern Lookout Platform",
"Flight Control Platform - Submarine",
"Star Island - Cave",
"Star Island - Lookout Platform",
"Star Belt Archipelago - Lookout Platform",
"Five-Star Isles - Lookout Platform - Destroy the Cannons",
"Five-Star Isles - Raft",
"Five-Star Isles - Submarine",
"Seven-Star Isles - Center Lookout Platform",
"Seven-Star Isles - Northern Lookout Platform",
"Seven-Star Isles - Southern Lookout Platform",
"Seven-Star Isles - Big Octo",
"Cyclops Reef - Lookout Platform - Defeat the Enemies",
"Two-Eye Reef - Lookout Platform",
"Two-Eye Reef - Big Octo Great Fairy",
"Five-Eye Reef - Lookout Platform",
"Six-Eye Reef - Lookout Platform - Destroy the Cannons",
"Six-Eye Reef - Submarine",
],
},
"Miniblins 2025": {

View File

@@ -76,10 +76,11 @@ at least normal.
A few presets are available on the [player options page](../player-options) for your convenience.
- **Tournament S7**: These are (as close to as possible) the settings used in the WWR Racing Server's
[Season 7 Tournament](https://docs.google.com/document/d/1mJj7an-DvpYilwNt-DdlFOy1fz5_NMZaPZvHeIekplc).
The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options
may seem intimidating, the preset also excludes several locations.
- **Tournament S8**: These are (as close to as possible) the settings used in the WWR Racing Server's
[Season 8 Tournament](https://docs.google.com/document/d/1b8F5DL3P5fgsQC_URiwhpMfqTpsGh2M-KmtTdXVigh4).
The preset features 4 required bosses (with Helmaroc King guaranteed required), dungeon entrance rando, hard obscurity
difficulty, and a variety of overworld checks. While the list of enabled progression options may seem intimidating,
the preset also excludes several locations and starts you with a handful of items.
- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's
[2025 Season of Miniblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to

View File

@@ -110,6 +110,14 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
else:
filler_pool.extend([item] * data.quantity)
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
if world.options.sword_mode == "start_with_sword":
precollected_items.append("Progressive Sword")
progression_pool.remove("Progressive Sword")
# Or, if it's swordless mode, remove all swords from the item pool.
elif world.options.sword_mode == "swordless":
useful_pool = [item for item in useful_pool if item != "Progressive Sword"]
# Assign useful and filler items to item pools in the world.
world.random.shuffle(useful_pool)
world.random.shuffle(filler_pool)
@@ -141,17 +149,6 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
pool.extend(progression_pool)
num_items_left_to_place -= len(progression_pool)
# If the player starts with a sword, add one to the precollected items list and remove one from the item pool.
if world.options.sword_mode == "start_with_sword":
precollected_items.append("Progressive Sword")
num_items_left_to_place += 1
pool.remove("Progressive Sword")
# Or, if it's swordless mode, remove all swords from the item pool.
elif world.options.sword_mode == "swordless":
while "Progressive Sword" in pool:
num_items_left_to_place += 1
pool.remove("Progressive Sword")
# Place useful items, then filler items to fill out the remaining locations.
pool.extend([world.get_filler_item_name(strict=False) for _ in range(num_items_left_to_place)])

View File

@@ -496,70 +496,74 @@ class WargrooveContext(CommonContext):
async def game_watcher(ctx: WargrooveContext):
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file == "deathLinkSend" and ctx.has_death_link:
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
failed_mission = f.read()
if ctx.slot is not None:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("victory") > -1:
victory = True
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSacrifice" or file == "unitSacrificeAI":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSacrificeAI":
stored_units_key = ctx.ai_stored_units_key
try:
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file == "deathLinkSend" and ctx.has_death_link:
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
unit_class = f.read()
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
await ctx.send_msgs(message)
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSummonRequestAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
if stored_units_key in ctx.stored_data:
stored_units = ctx.stored_data[stored_units_key]
if stored_units is None:
stored_units = []
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
if len(wg1_stored_units) != 0:
summoned_unit = random.choice(wg1_stored_units)
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
await ctx.send_msgs(message)
f.write(summoned_unit)
os.remove(os.path.join(ctx.game_communication_path, file))
failed_mission = f.read()
if ctx.slot is not None:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} failed {failed_mission}")
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
os.remove(os.path.join(ctx.game_communication_path, file))
if file.find("victory") > -1:
victory = True
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSacrifice" or file == "unitSacrificeAI":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSacrificeAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, file), 'r') as f:
unit_class = f.read()
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "add", "value": [unit_class[:64]]}]}]
await ctx.send_msgs(message)
os.remove(os.path.join(ctx.game_communication_path, file))
if file == "unitSummonRequestAI" or file == "unitSummonRequest":
if ctx.has_sacrifice_summon:
stored_units_key = ctx.player_stored_units_key
if file == "unitSummonRequestAI":
stored_units_key = ctx.ai_stored_units_key
with open(os.path.join(ctx.game_communication_path, "unitSummonResponse"), 'w') as f:
if stored_units_key in ctx.stored_data:
stored_units = ctx.stored_data[stored_units_key]
if stored_units is None:
stored_units = []
wg1_stored_units = [unit for unit in stored_units if unit in ctx.unit_classes]
if len(wg1_stored_units) != 0:
summoned_unit = random.choice(wg1_stored_units)
message = [{"cmd": 'Set', "key": stored_units_key,
"default": [],
"want_reply": True,
"operations": [{"operation": "remove", "value": summoned_unit[:64]}]}]
await ctx.send_msgs(message)
f.write(summoned_unit)
os.remove(os.path.join(ctx.game_communication_path, file))
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
except Exception as err:
logger.warn("Exception in communication thread, a check may not have been sent: " + str(err))
def print_error_and_close(msg):