From cbf4bbbca8633592c93e3352b0d3d5027f1002f4 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:17:31 -0500 Subject: [PATCH 01/57] OoT Adjuster: Remove per_slot_randoms (#4264) --- OoTAdjuster.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/OoTAdjuster.py b/OoTAdjuster.py index 9519b191e7..1581d65398 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -1,7 +1,6 @@ import tkinter as tk import argparse import logging -import random import os import zipfile from itertools import chain @@ -197,7 +196,6 @@ def set_icon(window): def adjust(args): # Create a fake multiworld and OOTWorld to use as a base multiworld = MultiWorld(1) - multiworld.per_slot_randoms = {1: random} ootworld = OOTWorld(multiworld, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): From 94438618495c0f9b051caea21ddb5d69c8c38c56 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sun, 19 Jan 2025 23:20:45 +0000 Subject: [PATCH 02/57] Zillion: Finalize item locations in either generate_output or fill_slot_data (#4121) Co-authored-by: Doug Hoskisson --- test/general/test_implemented.py | 2 +- worlds/zillion/__init__.py | 36 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 7abf995993..1082a02912 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase): """Tests that if a world creates slot data, it's json serializable.""" for game_name, world_type in AutoWorldRegister.world_types.items(): # has an await for generate_output which isn't being called - if game_name in {"Ocarina of Time", "Zillion"}: + if game_name in {"Ocarina of Time"}: continue multiworld = setup_solo_multiworld(world_type) with self.subTest(game=game_name, seed=multiworld.seed): diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 5a4e2bb48f..6fa5f86d07 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -119,8 +119,13 @@ class ZillionWorld(World): """ my_locations: list[ZillionLocation] = [] """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ - slot_data_ready: threading.Event - """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ + finalized_gen_data: GenData | None + """ Finalized generation data needed by `generate_output` and by `fill_slot_data`. """ + item_locations_finalization_lock: threading.Lock + """ + This lock is used in `generate_output` and `fill_slot_data` to ensure synchronized access to `finalized_gen_data`, + so that whichever is run first can finalize the item locations while the other waits. + """ logic_cache: ZillionLogicCache | None = None def __init__(self, world: MultiWorld, player: int) -> None: @@ -128,7 +133,8 @@ class ZillionWorld(World): self.logger = logging.getLogger("Zillion") self.lsi = ZillionWorld.LogStreamInterface(self.logger) self.zz_system = System() - self.slot_data_ready = threading.Event() + self.finalized_gen_data = None + self.item_locations_finalization_lock = threading.Lock() def _make_item_maps(self, start_char: Chars) -> None: _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) @@ -305,6 +311,19 @@ class ZillionWorld(World): self.zz_system.post_fill() + def finalize_item_locations_thread_safe(self) -> GenData: + """ + Call self.finalize_item_locations() and cache the result in a thread-safe manner so that either + `generate_output` or `fill_slot_data` can finalize item locations without concern for which of the two functions + is called first. + """ + # The lock is acquired when entering the context manager and released when exiting the context manager. + with self.item_locations_finalization_lock: + # If generation data has yet to be finalized, finalize it. + if self.finalized_gen_data is None: + self.finalized_gen_data = self.finalize_item_locations() + return self.finalized_gen_data + def finalize_item_locations(self) -> GenData: """ sync zilliandomizer item locations with AP item locations @@ -363,12 +382,7 @@ class ZillionWorld(World): def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use multiworld.random here. If you need any last-second randomization, use self.random instead.""" - try: - gen_data = self.finalize_item_locations() - except BaseException: - raise - finally: - self.slot_data_ready.set() + gen_data = self.finalize_item_locations_thread_safe() out_file_base = self.multiworld.get_out_file_name_base(self.player) @@ -392,9 +406,7 @@ class ZillionWorld(World): # TODO: tell client which canisters are keywords # so it can open and get those when restoring doors - self.slot_data_ready.wait() - assert self.zz_system.randomizer, "didn't get randomizer from generate_early" - game = self.zz_system.get_game() + game = self.finalize_item_locations_thread_safe().zz_game return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) # end of ordered Main.py calls From 563794ab832b813c1ff7bc51bf1b8c5a7243b17c Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 19 Jan 2025 15:29:13 -0800 Subject: [PATCH 03/57] Zillion: Use Useful Item Classification (#4179) --- worlds/zillion/__init__.py | 11 +++-------- worlds/zillion/item.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 6fa5f86d07..58f513ba6f 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -9,8 +9,7 @@ import logging from typing_extensions import override -from BaseClasses import ItemClassification, LocationProgressType, \ - MultiWorld, Item, CollectionState, Entrance, Tutorial +from BaseClasses import LocationProgressType, MultiWorld, Item, CollectionState, Entrance, Tutorial from .gen_data import GenData from .logic import ZillionLogicCache @@ -19,7 +18,7 @@ from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id -from .item import ZillionItem +from .item import ZillionItem, get_classification from .patch import ZillionPatch from zilliandomizer.system import System @@ -422,12 +421,8 @@ class ZillionWorld(World): self.logger.warning("warning: called `create_item` without calling `generate_early` first") assert self.id_to_zz_item, "failed to get item maps" - classification = ItemClassification.filler zz_item = self.id_to_zz_item[item_id] - if zz_item.required: - classification = ItemClassification.progression - if not zz_item.is_progression: - classification = ItemClassification.progression_skip_balancing + classification = get_classification(name, zz_item, self._item_counts) z_item = ZillionItem(name, classification, item_id, self.player, zz_item) return z_item diff --git a/worlds/zillion/item.py b/worlds/zillion/item.py index fdf0fa8ba2..5fa481ac36 100644 --- a/worlds/zillion/item.py +++ b/worlds/zillion/item.py @@ -1,6 +1,34 @@ +from typing import Counter from BaseClasses import Item, ItemClassification as IC from zilliandomizer.logic_components.items import Item as ZzItem +_useful_thresholds = { + "Apple": 9999, + "Champ": 9999, + "JJ": 9999, + "Win": 9999, + "Empty": 0, + "ID Card": 10, + "Red ID Card": 2, + "Floppy Disk": 7, + "Bread": 0, + "Opa-Opa": 20, + "Zillion": 8, + "Scope": 8, +} +""" make the item useful if the number in the item pool is below this number """ + + +def get_classification(name: str, zz_item: ZzItem, item_counts: Counter[str]) -> IC: + classification = IC.filler + if zz_item.required: + classification = IC.progression + if not zz_item.is_progression: + classification = IC.progression_skip_balancing + if item_counts[name] < _useful_thresholds.get(name, 0): + classification |= IC.useful + return classification + class ZillionItem(Item): game = "Zillion" From ca8ffe583d019c9a82928757263e5daee4cafbd3 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 19 Jan 2025 15:31:09 -0800 Subject: [PATCH 04/57] Zillion: Priority Dead Ends Feature (#4220) --- worlds/zillion/__init__.py | 14 ++++++++++++++ worlds/zillion/options.py | 15 +++++++++++++++ worlds/zillion/requirements.txt | 2 +- worlds/zillion/test/TestOptions.py | 17 ++++++++++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 58f513ba6f..d0064b9cb1 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -24,6 +24,7 @@ from .patch import ZillionPatch from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req +from zilliandomizer.map_gen.region_maker import DEAD_END_SUFFIX from zilliandomizer.options import Chars from worlds.AutoWorld import World, WebWorld @@ -172,6 +173,7 @@ class ZillionWorld(World): self.logic_cache = logic_cache w = self.multiworld self.my_locations = [] + dead_end_locations: list[ZillionLocation] = [] self.zz_system.randomizer.place_canister_gun_reqs() # low probability that place_canister_gun_reqs() results in empty 1st sphere @@ -224,6 +226,16 @@ class ZillionWorld(World): here.locations.append(loc) self.my_locations.append(loc) + if (( + zz_here.name.endswith(DEAD_END_SUFFIX) + ) or ( + (self.options.map_gen.value != self.options.map_gen.option_full) and + (loc.name in self.options.priority_dead_ends.vanilla_dead_ends) + ) or ( + loc.name in self.options.priority_dead_ends.always_dead_ends + )): + dead_end_locations.append(loc) + for zz_dest in zz_here.connections.keys(): dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name) dest = all_regions[dest_name] @@ -233,6 +245,8 @@ class ZillionWorld(World): queue.append(zz_dest) done.add(here.name) + if self.options.priority_dead_ends.value: + self.options.priority_locations.value |= {loc.name for loc in dead_end_locations} @override def create_items(self) -> None: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 22a6984722..13f3d43ab0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -272,6 +272,20 @@ class ZillionMapGen(Choice): return "full" +class ZillionPriorityDeadEnds(DefaultOnToggle): + """ + Single locations that are in a dead end behind a door + (example: vanilla Apple location) + are prioritized for progression items. + """ + display_name = "priority dead ends" + + vanilla_dead_ends: ClassVar = frozenset(("E-5 top far right", "J-4 top left")) + """ dead ends when not generating these rooms """ + always_dead_ends: ClassVar = frozenset(("A-6 top right",)) + """ dead ends in rooms that never get generated """ + + @dataclass class ZillionOptions(PerGameCommonOptions): continues: ZillionContinues @@ -293,6 +307,7 @@ class ZillionOptions(PerGameCommonOptions): skill: ZillionSkill starting_cards: ZillionStartingCards map_gen: ZillionMapGen + priority_dead_ends: ZillionPriorityDeadEnds room_gen: Removed diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index d6b01ac107..4f79626c9a 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@96d9a20f8278cee64bb4db859fbd874e0f332d36#0.9.1 typing-extensions>=4.7, <5 diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index 3820c32dd0..904063fd3c 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -1,6 +1,7 @@ from . import ZillionTestBase -from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate +from .. import ZillionWorld +from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, ZillionPriorityDeadEnds, validate from zilliandomizer.options import VBLR_CHOICES @@ -28,3 +29,17 @@ class OptionsTest(ZillionTestBase): assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options + + +class DeadEndsTest(ZillionTestBase): + def test_vanilla_dead_end_names(self) -> None: + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + for loc_name in ZillionPriorityDeadEnds.vanilla_dead_ends: + assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}" + + def test_always_dead_end_names(self) -> None: + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + for loc_name in ZillionPriorityDeadEnds.always_dead_ends: + assert any(loc.name == loc_name for loc in z_world.my_locations), f"{loc_name=} {z_world.my_locations=}" From 130232b45707be8d9b8d7570344a8c3700d0f87b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:56:37 +0100 Subject: [PATCH 05/57] Core: Make log time an optional arg & setting for Generate.py as well #4312 --- Generate.py | 6 ++++-- settings.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Generate.py b/Generate.py index d6611b0f8a..b057db25a3 100644 --- a/Generate.py +++ b/Generate.py @@ -42,7 +42,9 @@ def mystery_argparse(): help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) - parser.add_argument('--log_level', default='info', help='Sets log level') + parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') + parser.add_argument('--log_time', help="Add timestamps to STDOUT", + default=defaults.logtime, action='store_true') parser.add_argument("--csv_output", action="store_true", help="Output rolled player options to csv (made for async multiworld).") parser.add_argument("--plando", default=defaults.plando_options, @@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: seed = get_seed(args.seed) - Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time) random.seed(seed) seed_name = get_seed_name(random) diff --git a/settings.py b/settings.py index 04d8760c3c..12dace632c 100644 --- a/settings.py +++ b/settings.py @@ -678,6 +678,8 @@ class GeneratorOptions(Group): race: Race = Race(0) plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") panic_method: PanicMethod = PanicMethod("swap") + loglevel: str = "info" + logtime: bool = False class SNIOptions(Group): From 39847c55027117e43d44bd034585e21b79d4ed20 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Jan 2025 02:05:07 +0100 Subject: [PATCH 06/57] WebHost: sort slots by player_id in api blueprint (#4354) --- WebHostLib/api/__init__.py | 4 ++-- WebHostLib/api/user.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cf05e87374..d0b9d05c16 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -3,13 +3,13 @@ from typing import List, Tuple from flask import Blueprint -from ..models import Seed +from ..models import Seed, Slot api_endpoints = Blueprint('api', __name__, url_prefix="/api") def get_players(seed: Seed) -> List[Tuple[str, str]]: - return [(slot.player_name, slot.game) for slot in seed.slots] + return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)] from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 116d3afa22..0ddb6fe83e 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -30,4 +30,4 @@ def get_seeds(): "creation_time": seed.creation_time, "players": get_players(seed.slots), }) - return jsonify(response) \ No newline at end of file + return jsonify(response) From eb3c3d6bf2fb9b161723c7e7ae2990ab0ab7b6bd Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 20:12:44 -0500 Subject: [PATCH 07/57] FFMQ: Adds Items Accessibility (#4322) --- worlds/ffmq/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index 41c397315f..4dcf1467d6 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions +from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions from dataclasses import dataclass @@ -324,6 +324,7 @@ class KaelisMomFightsMinotaur(Toggle): @dataclass class FFMQOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility logic: Logic brown_boxes: BrownBoxes sky_coin_mode: SkyCoinMode From 992841a951d71bbee05e28655e587567ab9555ca Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 19 Jan 2025 20:18:36 -0500 Subject: [PATCH 08/57] CommonClient: abstract url handling so it's importable (#4068) Co-authored-by: Doug Hoskisson Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> --- CommonClient.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index b43bf57d19..f6b2623f8c 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -31,6 +31,7 @@ import ssl if typing.TYPE_CHECKING: import kvui + import argparse logger = logging.getLogger("Client") @@ -1048,6 +1049,32 @@ def get_base_parser(description: typing.Optional[str] = None): return parser +def handle_url_arg(args: "argparse.Namespace", + parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace": + """ + Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient + If alternate data is required the urlparse response is saved back to args.url if valid + """ + if not args.url: + return args + + url = urllib.parse.urlparse(args.url) + if url.scheme != "archipelago": + if not parser: + parser = get_base_parser() + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + return args + + args.url = url + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + + return args + + def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry @@ -1089,17 +1116,7 @@ def run_as_textclient(*args): parser.add_argument("url", nargs="?", help="Archipelago connection url") args = parser.parse_args(args) - # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost - if args.url: - url = urllib.parse.urlparse(args.url) - if url.scheme == "archipelago": - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) - else: - parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + args = handle_url_arg(args, parser=parser) # use colorama to display colored text highlighting on windows colorama.init() From 4fa8c432666039ec7cd82e88ba2fd29a62bacbd9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:06:09 -0500 Subject: [PATCH 09/57] FFMQ: Fix collect_item (#4433) * Fix FFMQ collect_item --- worlds/ffmq/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index 3c58487265..58dc4bf13e 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -152,14 +152,23 @@ class FFMQWorld(World): return FFMQItem(name, self.player) def collect_item(self, state, item, remove=False): + if not item.advancement: + return None if "Progressive" in item.name: i = item.code - 256 + if remove: + if state.has(self.item_id_to_name[i+1], self.player): + if state.has(self.item_id_to_name[i+2], self.player): + return self.item_id_to_name[i+2] + return self.item_id_to_name[i+1] + return self.item_id_to_name[i] + if state.has(self.item_id_to_name[i], self.player): if state.has(self.item_id_to_name[i+1], self.player): return self.item_id_to_name[i+2] return self.item_id_to_name[i+1] return self.item_id_to_name[i] - return item.name if item.advancement else None + return item.name def modify_multidata(self, multidata): # wait for self.rom_name to be available. From a2fbf856ff1f6d274468b879521c72f78762aa3a Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:07:01 -0500 Subject: [PATCH 10/57] SMZ3: Change locality options earlier (#4424) --- worlds/smz3/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7ebec7d4e4..dca105b162 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -217,6 +217,10 @@ class SMZ3World(World): SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys()) self.multiworld.state.smz3state[self.player] = TotalSMZ3Item.Progression([]) + + if not self.smz3World.Config.Keysanity: + # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local + self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) def create_items(self): self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World) @@ -233,8 +237,6 @@ class SMZ3World(World): progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: progressionItems = self.progression - # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local - self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) From d5cd95c7fba516480d303df3a842bffd98d70ff4 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 20 Jan 2025 03:01:45 -0500 Subject: [PATCH 11/57] Docs: Clarify usage of slot data for trackers in World API doc (#3986) * Clarify usage of slot data for trackers in world API. * Typo. * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update docs/world api.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Keep to 120 char lines. --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/world api.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 487c5b4a36..762189a908 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -835,14 +835,16 @@ def generate_output(self, output_directory: str) -> None: ### Slot Data -If the game client needs to know information about the generated seed, a preferred method of transferring the data -is through the slot data. This is filled with the `fill_slot_data` method of your world by returning -a `dict` with `str` keys that can be serialized with json. -But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client -once it has successfully [connected](network%20protocol.md#connected). +If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data +is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with +`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is +absolutely necessary. Slot data is sent to your client once it has successfully +[connected](network%20protocol.md#connected). + If you need to know information about locations in your world, instead of propagating the slot data, it is preferable -to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most -common usage of slot data is sending option results that the client needs to be aware of. +to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding +item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients +that request it. The most common usage of slot data is sending option results that the client needs to be aware of. ```python def fill_slot_data(self) -> Dict[str, Any]: From 4f77abac4f567048d909a049004d03aa303c043c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 09:53:30 -0500 Subject: [PATCH 12/57] TUNIC: Fix failure in 1-player grass (#4520) * Fix failure in 1-player grass * Update worlds/tunic/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/tunic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 388a44113a..1394c11c90 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -411,7 +411,7 @@ class TunicWorld(World): def stage_pre_fill(cls, multiworld: MultiWorld) -> None: tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") if world.options.local_fill.value > 0] - if tunic_fill_worlds: + if tunic_fill_worlds and multiworld.players > 1: grass_fill: List[TunicItem] = [] non_grass_fill: List[TunicItem] = [] grass_fill_locations: List[Location] = [] From 96f469c73792199f84fe3128244c0dfa73331df4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 10:04:39 -0500 Subject: [PATCH 13/57] TUNIC: Fix hero relics not being prog if hex quest is on in combat logic #4509 --- worlds/tunic/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 1394c11c90..96d3c10b82 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -331,10 +331,11 @@ class TunicWorld(World): remove_filler(items_to_create[gold_hexagon]) - # Sort for deterministic order - for hero_relic in sorted(item_name_groups["Hero Relics"]): - tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) - items_to_create[hero_relic] = 0 + if not self.options.combat_logic: + # Sort for deterministic order + for hero_relic in sorted(item_name_groups["Hero Relics"]): + tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) + items_to_create[hero_relic] = 0 if not self.options.ability_shuffling: # Sort for deterministic order From 436c0a41048f6f10084387e61f73995381808eed Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:07:15 +0100 Subject: [PATCH 14/57] Core: Add connect_entrances world step/stage (#4420) * Add connect_entrances * update ER docs * fix that test, but also ew * Add a test that asserts the new finalization * Rewrite test a bit * rewrite some more * blank line * rewrite rewrite rewrite * rewrite rewrite rewrite * RE. WRITE. * oops * Bruh * I guess, while we're at it * giga oops * It's been a long day * Switch KH1 over to this design with permission of GICU * Revert * Oops * Bc I like it * Update locations.py --- Main.py | 3 ++- docs/entrance randomization.md | 18 ++++++---------- docs/world api.md | 3 +++ test/benchmark/locations.py | 10 ++++++++- test/general/__init__.py | 10 ++++++++- test/general/test_entrances.py | 36 +++++++++++++++++++++++++++++++ test/general/test_items.py | 4 ++-- test/general/test_locations.py | 6 ++++++ test/general/test_reachability.py | 4 ++-- worlds/AutoWorld.py | 4 ++++ worlds/kh1/Regions.py | 3 +++ worlds/kh1/__init__.py | 5 ++++- 12 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 test/general/test_entrances.py diff --git a/Main.py b/Main.py index d105bd4ad0..d0e7a7f879 100644 --- a/Main.py +++ b/Main.py @@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.local_items.value = set() - + + AutoWorld.call_all(multiworld, "connect_entrances") AutoWorld.call_all(multiworld, "generate_basic") # remove starting inventory from pool items. diff --git a/docs/entrance randomization.md b/docs/entrance randomization.md index 9e3e281bcc..0f9d764716 100644 --- a/docs/entrance randomization.md +++ b/docs/entrance randomization.md @@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups) #### When to call `randomize_entrances` -The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading. +The correct step for this is `World.connect_entrances`. -ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures. -This means 2 things about when you can call ER: -1. You must supply your item pool before calling ER, or call ER before setting any rules which require items. -2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules - and create your events before you call ER if you want to guarantee a correct output. - -If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also -a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER -in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or -generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as -well. +Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`. +However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better +together. +These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`. +It is fine for your Entrances to be connected differently or not at all before this step. #### Informing your client about randomized entrances diff --git a/docs/world api.md b/docs/world api.md index 762189a908..90fe446d61 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -490,6 +490,9 @@ In addition, the following methods can be implemented and are called in this ord after this step. Locations cannot be moved to different regions after this step. * `set_rules(self)` called to set access and item rules on locations and entrances. +* `connect_entrances(self)` + by the end of this step, all entrances must exist and be connected to their source and target regions. + Entrance randomization should be done here. * `generate_basic(self)` player-specific randomization that does not affect logic can be done here. * `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py index f2209eb689..857e188236 100644 --- a/test/benchmark/locations.py +++ b/test/benchmark/locations.py @@ -18,7 +18,15 @@ def run_locations_benchmark(): class BenchmarkRunner: gen_steps: typing.Tuple[str, ...] = ( - "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + "generate_early", + "create_regions", + "create_items", + "set_rules", + "connect_entrances", + "generate_basic", + "pre_fill", + ) + rule_iterations: int = 100_000 if sys.version_info >= (3, 9): diff --git a/test/general/__init__.py b/test/general/__init__.py index 8afd849765..6c4d5092cf 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul from worlds import network_data_package from worlds.AutoWorld import World, call_all -gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") +gen_steps = ( + "generate_early", + "create_regions", + "create_items", + "set_rules", + "connect_entrances", + "generate_basic", + "pre_fill", +) def setup_solo_multiworld( diff --git a/test/general/test_entrances.py b/test/general/test_entrances.py new file mode 100644 index 0000000000..72161dfbde --- /dev/null +++ b/test/general/test_entrances.py @@ -0,0 +1,36 @@ +import unittest +from worlds.AutoWorld import AutoWorldRegister, call_all, World +from . import setup_solo_multiworld + + +class TestBase(unittest.TestCase): + def test_entrance_connection_steps(self): + """Tests that Entrances are connected and not changed after connect_entrances.""" + def get_entrance_name_to_source_and_target_dict(world: World): + return [ + (entrance.name, entrance.parent_region, entrance.connected_region) + for entrance in world.get_entrances() + ] + + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances") + additional_steps = ("generate_basic", "pre_fill") + + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + + original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1]) + + self.assertTrue( + all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances), + f"{game_name} had unconnected entrances after connect_entrances" + ) + + for step in additional_steps: + with self.subTest("Step", step=step): + call_all(multiworld, step) + step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1]) + + self.assertEqual( + original_entrances, step_entrances, f"{game_name} modified entrances during {step}" + ) diff --git a/test/general/test_items.py b/test/general/test_items.py index 64ce1b6997..91d334e968 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -67,7 +67,7 @@ class TestBase(unittest.TestCase): def test_itempool_not_modified(self): """Test that worlds don't modify the itempool after `create_items`""" gen_steps = ("generate_early", "create_regions", "create_items") - additional_steps = ("set_rules", "generate_basic", "pre_fill") + additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3") worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games} @@ -84,7 +84,7 @@ class TestBase(unittest.TestCase): def test_locality_not_modified(self): """Test that worlds don't modify the locality of items after duplicates are resolved""" gen_steps = ("generate_early", "create_regions", "create_items") - additional_steps = ("set_rules", "generate_basic", "pre_fill") + additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill") worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()} for game_name, world_type in worlds_to_test.items(): with self.subTest("Game", game=game_name): diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 4b95ebd22c..37ae94e003 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -45,6 +45,12 @@ class TestBase(unittest.TestCase): self.assertEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during rule creation") + call_all(multiworld, "connect_entrances") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during rule creation") + self.assertEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during rule creation") + call_all(multiworld, "generate_basic") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during generate_basic") diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index fafa702389..b45a2bdfc0 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -2,11 +2,11 @@ import unittest from BaseClasses import CollectionState from worlds.AutoWorld import AutoWorldRegister -from . import setup_solo_multiworld +from . import setup_solo_multiworld, gen_steps class TestBase(unittest.TestCase): - gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] + gen_steps = gen_steps default_settings_unreachable_regions = { "A Link to the Past": { diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index a510717920..0fcacc8ab3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister): """Method for setting the rules on the World's regions and locations.""" pass + def connect_entrances(self) -> None: + """Method to finalize the source and target regions of the World's entrances""" + pass + def generate_basic(self) -> None: """ Useful for randomizing things that don't affect logic but are better to be determined before the output stage. diff --git a/worlds/kh1/Regions.py b/worlds/kh1/Regions.py index a6f85fe617..6189adf207 100644 --- a/worlds/kh1/Regions.py +++ b/worlds/kh1/Regions.py @@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options): for name, data in regions.items(): multiworld.regions.append(create_region(multiworld, player, name, data)) + +def connect_entrances(multiworld: MultiWorld, player: int): multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player)) multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player)) multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player)) @@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options): multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player)) multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player)) + def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData): region = Region(name, player, multiworld) if data.locations: diff --git a/worlds/kh1/__init__.py b/worlds/kh1/__init__.py index 63b4575568..3b498acf46 100644 --- a/worlds/kh1/__init__.py +++ b/worlds/kh1/__init__.py @@ -6,7 +6,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups from .Options import KH1Options, kh1_option_groups -from .Regions import create_regions +from .Regions import connect_entrances, create_regions from .Rules import set_rules from .Presets import kh1_option_presets from worlds.LauncherComponents import Component, components, Type, launch_subprocess @@ -242,6 +242,9 @@ class KH1World(World): def create_regions(self): create_regions(self.multiworld, self.player, self.options) + + def connect_entrances(self): + connect_entrances(self.multiworld, self.player) def generate_early(self): value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"] From 05d1b2129a9bdef4709f5a5b892324faa5c6b3bd Mon Sep 17 00:00:00 2001 From: "Chris J." Date: Mon, 20 Jan 2025 11:18:09 -0500 Subject: [PATCH 15/57] Docs: Update ID Overlapping Docs (#4447) --- docs/world api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 90fe446d61..da74be70fb 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 -letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs. -Locations and items can share IDs, so typically a game's locations and items start at the same ID. +letter or symbol). The ID needs to be unique across all locations within the game. +Locations and items can share IDs, and locations can share IDs with other games' locations. World-specific IDs must be in the range 1 to 253-1; IDs ≤ 0 are global and reserved. @@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit and satisfy progression balancing. The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they -will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). +will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). +The ID thus also needs to be unique across all items with different names within the game. +Items and locations can share IDs, and items can share IDs with other games' items. Other classifications include: From 823b17c386f1dce27e537e1c0263b0bc8670c377 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 20 Jan 2025 11:44:39 -0500 Subject: [PATCH 16/57] TUNIC: Make grass go in the regular location name group too (#4504) * Make grass go in the normal loc group too * Make it not overwrite old groups --- worlds/tunic/__init__.py | 3 ++- worlds/tunic/grass.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 96d3c10b82..087e17c3e4 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -78,7 +78,8 @@ class TunicWorld(World): settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups - location_name_groups.update(grass_location_name_groups) + for group_name, members in grass_location_name_groups.items(): + location_name_groups.setdefault(group_name, set()).update(members) item_name_to_id = item_name_to_id location_name_to_id = standard_location_name_to_id.copy() diff --git a/worlds/tunic/grass.py b/worlds/tunic/grass.py index 592b2938b1..eb688199dc 100644 --- a/worlds/tunic/grass.py +++ b/worlds/tunic/grass.py @@ -7767,8 +7767,10 @@ grass_location_name_to_id: Dict[str, int] = {name: location_base_id + 302 + inde grass_location_name_groups: Dict[str, Set[str]] = {} for loc_name, loc_data in grass_location_table.items(): - loc_group_name = loc_name.split(" - ", 1)[0] + " Grass" - grass_location_name_groups.setdefault(loc_group_name, set()).add(loc_name) + area_name = loc_name.split(" - ", 1)[0] + # adding it to the normal location group and a grass-only one + grass_location_name_groups.setdefault(area_name, set()).add(loc_name) + grass_location_name_groups.setdefault(area_name + " Grass", set()).add(loc_name) def can_break_grass(state: CollectionState, world: "TunicWorld") -> bool: From e2b942139a5ed1908725812edd52aa479a5d835b Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 13:10:29 -0500 Subject: [PATCH 17/57] HK: Save GrubHuntGoal by value (#4521) --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 7e9b7442a7..daf177fb8d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -209,7 +209,7 @@ class HKWorld(World): # defaulting so completion condition isn't incorrect before pre_fill self.grub_count = ( 46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] - else options.GrubHuntGoal + else options.GrubHuntGoal.value ) self.grub_player_count = {self.player: self.grub_count} From a126dee06824d3fee890eecfbf8a5d4870261e67 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 17:42:12 -0500 Subject: [PATCH 18/57] HK: some stuff ruff and pycodestyle complained about (#4523) --- worlds/hk/Options.py | 2 +- worlds/hk/__init__.py | 52 ++++++++++++++++--------------- worlds/hk/test/__init__.py | 1 - worlds/hk/test/test_grub_count.py | 3 +- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 53dda96e2b..e76e7eba9d 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -333,7 +333,7 @@ class PlandoCharmCosts(OptionDict): continue try: self.value[key] = CharmCost.from_any(data).value - except ValueError as ex: + except ValueError: # will fail schema afterwords self.value[key] = data diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index daf177fb8d..4a0da109fa 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -7,22 +7,22 @@ import itertools import operator from collections import defaultdict, Counter -logger = logging.getLogger("Hollow Knight") - -from .Items import item_table, lookup_type_to_names, item_name_groups -from .Regions import create_regions +from .Items import item_table, item_name_groups from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ shop_to_option, HKOptions, GrubHuntGoal -from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ - event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs +from .ExtractedData import locations, starts, multi_locations, event_names, item_effects, connectors, \ + vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState +from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, \ + CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld from settings import Group, Bool +logger = logging.getLogger("Hollow Knight") + class HollowKnightSettings(Group): class DisableMapModSpoilers(Bool): @@ -160,7 +160,7 @@ class HKWeb(WebWorld): class HKWorld(World): - """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, + """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, searching for riches, or glory, or answers to old secrets. As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. @@ -231,7 +231,6 @@ class HKWorld(World): def create_regions(self): menu_region: Region = create_region(self.multiworld, self.player, 'Menu') self.multiworld.regions.append(menu_region) - # wp_exclusions = self.white_palace_exclusions() # check for any goal that godhome events are relevant to all_event_names = event_names.copy() @@ -241,21 +240,17 @@ class HKWorld(World): # Link regions for event_name in sorted(all_event_names): - #if event_name in wp_exclusions: - # continue loc = HKLocation(self.player, event_name, None, menu_region) loc.place_locked_item(HKItem(event_name, - True, #event_name not in wp_exclusions, + True, None, "Event", self.player)) menu_region.locations.append(loc) for entry_transition, exit_transition in connectors.items(): - #if entry_transition in wp_exclusions: - # continue if exit_transition: # if door logic fulfilled -> award vanilla target as event loc = HKLocation(self.player, entry_transition, None, menu_region) loc.place_locked_item(HKItem(exit_transition, - True, #exit_transition not in wp_exclusions, + True, None, "Event", self.player)) menu_region.locations.append(loc) @@ -292,7 +287,10 @@ class HKWorld(World): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) + item = (self.create_item(item_name) + if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations + else self.create_event(item_name) + ) if location_name == "Start": if item_name in randomized_starting_items: @@ -347,8 +345,8 @@ class HKWorld(World): randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) - for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): + for shop, shop_locations in self.created_multi_locations.items(): + for _ in range(len(shop_locations), getattr(self.options, shop_to_option[shop]).value): self.create_location(shop) unfilled_locations += 1 @@ -358,7 +356,7 @@ class HKWorld(World): # Add additional shop items, as needed. if additional_shop_items > 0: - shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) + shops = [shop for shop, shop_locations in self.created_multi_locations.items() if len(shop_locations) < 16] if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') @@ -380,8 +378,8 @@ class HKWorld(World): self.sort_shops_by_cost() def sort_shops_by_cost(self): - for shop, locations in self.created_multi_locations.items(): - randomized_locations = list(loc for loc in locations if not loc.vanilla) + for shop, shop_locations in self.created_multi_locations.items(): + randomized_locations = [loc for loc in shop_locations if not loc.vanilla] prices = sorted( (loc.costs for loc in randomized_locations), key=lambda costs: (len(costs),) + tuple(costs.values()) @@ -405,7 +403,7 @@ class HKWorld(World): return {k: v for k, v in weights.items() if v} random = self.random - hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value + hybrid_chance = getattr(self.options, "CostSanityHybridChance").value weights = { data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() @@ -493,7 +491,11 @@ class HKWorld(World): worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] if worlds: grubs = [item for item in multiworld.get_items() if item.name == "Grub"] - all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + all_grub_players = [ + world.player + for world in worlds + if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] + ] if all_grub_players: group_lookup = defaultdict(set) @@ -668,8 +670,8 @@ class HKWorld(World): ): spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") else: - for shop_name, locations in hk_world.created_multi_locations.items(): - for loc in locations: + for shop_name, shop_locations in hk_world.created_multi_locations.items(): + for loc in shop_locations: spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str: diff --git a/worlds/hk/test/__init__.py b/worlds/hk/test/__init__.py index c41d20127f..67591001a7 100644 --- a/worlds/hk/test/__init__.py +++ b/worlds/hk/test/__init__.py @@ -2,7 +2,6 @@ import typing from argparse import Namespace from BaseClasses import CollectionState, MultiWorld from Options import ItemLinks -from test.bases import WorldTestBase from worlds.AutoWorld import AutoWorldRegister, call_all from .. import HKWorld diff --git a/worlds/hk/test/test_grub_count.py b/worlds/hk/test/test_grub_count.py index dba15b614d..a58293c078 100644 --- a/worlds/hk/test/test_grub_count.py +++ b/worlds/hk/test/test_grub_count.py @@ -1,5 +1,6 @@ -from . import linkedTestHK, WorldTestBase +from test.bases import WorldTestBase from Options import ItemLinks +from . import linkedTestHK class test_grubcount_limited(linkedTestHK, WorldTestBase): From 33fd9de281f0d4bbbda5f6134eaa7759a3978cb4 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 20 Jan 2025 18:56:20 -0500 Subject: [PATCH 19/57] Core: Add Retry to Priority Fill (#4477) * adds a retry to priority fill in case the one item per player optimization would cause the priority fill to fail to find valid placements * Update Fill.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Fill.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 0da2d5def9..d1773c8213 100644 --- a/Fill.py +++ b/Fill.py @@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld, # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, single_player_placement=single_player, swap=False, on_place=mark_for_locking, - name="Priority", one_item_per_player=False) + name="Priority", one_item_per_player=True, allow_partial=True) + + if prioritylocations: + # retry with one_item_per_player off because some priority fills can fail to fill with that optimization + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry", one_item_per_player=False) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations From edacb17171478be7b512b61deac520c394e34b3f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Jan 2025 16:12:53 +0100 Subject: [PATCH 20/57] Factorio: remove debug print (#4533) --- worlds/factorio/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8f8abeb292..a2bc518ae3 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -280,9 +280,6 @@ class Factorio(World): self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) - for tech_name in victory_tech_names: - if not self.multiworld.get_all_state(True).has(tech_name, player): - print(tech_name) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) def get_recipe(self, name: str) -> Recipe: From 1a1b7e9cf4c14729f6d3cc0a06f69260610e50e0 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 21 Jan 2025 12:39:08 -0500 Subject: [PATCH 21/57] TUNIC: Reduce range end for local_fill option #4534 --- worlds/tunic/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 14bf5d8a18..d2ea828037 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -173,7 +173,7 @@ class LocalFill(NamedRange): internal_name = "local_fill" display_name = "Local Fill Percent" range_start = 0 - range_end = 100 + range_end = 98 special_range_names = { "default": -1 } From 949527f9cb45ac7248d6aa7a73ec2ff980a64fcd Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:28:33 -0500 Subject: [PATCH 22/57] KH2: Bug fixes and game update future proofing (#4075) Co-authored-by: qwint --- worlds/kh2/Client.py | 136 ++++++++++++++++++++++-------------- worlds/kh2/Regions.py | 2 +- worlds/kh2/Rules.py | 6 +- worlds/kh2/docs/setup_en.md | 2 +- 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 3ea47e40eb..0254d46e93 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -5,8 +5,10 @@ ModuleUpdate.update() import os import asyncio import json +import requests from pymem import pymem -from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot +from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \ + SupportAbility_Table, ActionAbility_Table, all_weapon_slot from .Names import ItemName from .WorldLocations import * @@ -82,6 +84,7 @@ class KH2Context(CommonContext): } self.kh2seedname = None self.kh2slotdata = None + self.mem_json = None self.itemamount = {} if "localappdata" in os.environ: self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") @@ -178,7 +181,8 @@ class KH2Context(CommonContext): self.base_accessory_slots = 1 self.base_armor_slots = 1 self.base_item_slots = 3 - self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, + 0x2770, 0x2772] async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -340,12 +344,8 @@ class KH2Context(CommonContext): self.locations_checked |= new_locations if cmd in {"DataPackage"}: - self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] - self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} - self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] - self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} - self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] - + if "Kingdom Hearts 2" in args["data"]["games"]: + self.data_package_kh2_cache(args) if "KeybladeAbilities" in self.kh2slotdata.keys(): # sora ability to slot self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"]) @@ -359,24 +359,9 @@ class KH2Context(CommonContext): self.all_weapon_location_id = set(all_weapon_location_id) try: - self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - if self.kh2_game_version is None: - if self.kh2_read_string(0x09A9830, 4) == "KH2J": - self.kh2_game_version = "STEAM" - self.Now = 0x0717008 - self.Save = 0x09A9830 - self.Slot1 = 0x2A23518 - self.Journal = 0x7434E0 - self.Shop = 0x7435D0 - - elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": - self.kh2_game_version = "EGS" - else: - self.kh2_game_version = None - logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") - if self.kh2_game_version is not None: - logger.info(f"You are now auto-tracking. {self.kh2_game_version}") - self.kh2connected = True + if not self.kh2: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + self.get_addresses() except Exception as e: if self.kh2connected: @@ -385,6 +370,13 @@ class KH2Context(CommonContext): self.serverconneced = True asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}])) + def data_package_kh2_cache(self, args): + self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] + self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} + self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] + self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} + self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] + async def checkWorldLocations(self): try: currentworldint = self.kh2_read_byte(self.Now) @@ -425,7 +417,6 @@ class KH2Context(CommonContext): 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels] } - # TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3 for i in range(6): for location, data in formDict[i][1].items(): formlevel = self.kh2_read_byte(self.Save + data.addrObtained) @@ -469,9 +460,11 @@ class KH2Context(CommonContext): if locationName in self.chest_set: if locationName in self.location_name_to_worlddata.keys(): locationData = self.location_name_to_worlddata[locationName] - if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: + if self.kh2_read_byte( + self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: roomData = self.kh2_read_byte(self.Save + locationData.addrObtained) - self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex) + self.kh2_write_byte(self.Save + locationData.addrObtained, + roomData | 0x01 << locationData.bitIndex) except Exception as e: if self.kh2connected: @@ -494,6 +487,9 @@ class KH2Context(CommonContext): async def give_item(self, item, location): try: # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + #sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts + while not self.lookup_id_to_item: + await asyncio.sleep(0.5) itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -637,7 +633,8 @@ class KH2Context(CommonContext): item_data = self.item_name_to_data[item_name] # if the inventory slot for that keyblade is less than the amount they should have, # and they are not in stt - if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13: + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte( + self.Save + 0x1CFF) != 13: # Checking form anchors for the keyblade to remove extra keyblades if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \ or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \ @@ -738,7 +735,8 @@ class KH2Context(CommonContext): item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}: + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte( + self.Shop) in {10, 8}: self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: @@ -797,7 +795,8 @@ class KH2Context(CommonContext): # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: - if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 + if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte( + self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) except Exception as e: @@ -806,10 +805,59 @@ class KH2Context(CommonContext): logger.info(e) logger.info("line 840") + def get_addresses(self): + if not self.kh2connected and self.kh2 is not None: + if self.kh2_game_version is None: + + if self.kh2_read_string(0x09A9830, 4) == "KH2J": + self.kh2_game_version = "STEAM" + self.Now = 0x0717008 + self.Save = 0x09A9830 + self.Slot1 = 0x2A23518 + self.Journal = 0x7434E0 + self.Shop = 0x7435D0 + elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": + self.kh2_game_version = "EGS" + else: + if self.game_communication_path: + logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment") + #if mem addresses file is found then check version and if old get new one + kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json") + if not os.path.exists(kh2memaddresses_path): + mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json") + if mem_resp.status_code == 200: + self.mem_json = json.loads(mem_resp.content) + with open(kh2memaddresses_path, + 'w') as f: + f.write(json.dumps(self.mem_json, indent=4)) + else: + with open(kh2memaddresses_path, 'r') as f: + self.mem_json = json.load(f) + if self.mem_json: + for key in self.mem_json.keys(): + + if self.kh2_read_string(eval(self.mem_json[key]["GameVersionCheck"]), 4) == "KH2J": + self.Now = eval(self.mem_json[key]["Now"]) + self.Save=eval(self.mem_json[key]["Save"]) + self.Slot1 = eval(self.mem_json[key]["Slot1"]) + self.Journal = eval(self.mem_json[key]["Journal"]) + self.Shop = eval(self.mem_json[key]["Shop"]) + self.kh2_game_version = key + + if self.kh2_game_version is not None: + logger.info(f"You are now auto-tracking {self.kh2_game_version}") + self.kh2connected = True + else: + logger.info("Your game version does not match what the client requires. Check in the " + "kingdom-hearts-2-final-mix channel for more information on correcting the game " + "version.") + self.kh2connected = False + def finishedGame(ctx: KH2Context): if ctx.kh2slotdata['FinalXemnas'] == 1: - if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ + if not ctx.final_xemnas and ctx.kh2_read_byte( + ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: ctx.final_xemnas = True # three proofs @@ -843,7 +891,8 @@ def finishedGame(ctx: KH2Context): for boss in ctx.kh2slotdata["hitlist"]: if boss in locations: ctx.hitlist_bounties += 1 - if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]: + if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"][ + "Bounty"] >= ctx.kh2slotdata["BountyRequired"]: if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) @@ -894,24 +943,7 @@ async def kh2_watcher(ctx: KH2Context): while not ctx.kh2connected and ctx.serverconneced: await asyncio.sleep(15) ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - if ctx.kh2 is not None: - if ctx.kh2_game_version is None: - if ctx.kh2_read_string(0x09A9830, 4) == "KH2J": - ctx.kh2_game_version = "STEAM" - ctx.Now = 0x0717008 - ctx.Save = 0x09A9830 - ctx.Slot1 = 0x2A23518 - ctx.Journal = 0x7434E0 - ctx.Shop = 0x7435D0 - - elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J": - ctx.kh2_game_version = "EGS" - else: - ctx.kh2_game_version = None - logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") - if ctx.kh2_game_version is not None: - logger.info(f"You are now auto-tracking {ctx.kh2_game_version}") - ctx.kh2connected = True + ctx.get_addresses() except Exception as e: if ctx.kh2connected: ctx.kh2connected = False diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 7fc2ad8a87..e6e8a7b2f6 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -540,7 +540,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = { LocationName.SephirothFenrir, LocationName.SephiEventLocation ], - RegionName.CoR: [ + RegionName.CoR: [ #todo: make logic for getting these checks. LocationName.CoRDepthsAPBoost, LocationName.CoRDepthsPowerCrystal, LocationName.CoRDepthsFrostCrystal, diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 0f26b56d0e..767c564341 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -194,8 +194,8 @@ class KH2WorldRules(KH2Rules): RegionName.Oc: lambda state: self.oc_unlocked(state, 1), RegionName.Oc2: lambda state: self.oc_unlocked(state, 2), + #twtnw1 is actually the roxas fight region thus roxas requires 1 way to the dawn RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2), - # These will be swapped and First Visit lock for twtnw is in development. # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2), RegionName.Ht: lambda state: self.ht_unlocked(state, 1), @@ -919,8 +919,8 @@ class KH2FightRules(KH2Rules): # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus sephiroth_rules = { - "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, - "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state), + "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([gap_closer], state) >= 1, "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, } return sephiroth_rules[self.fight_logic] diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index cb80ec6098..bee60bd36b 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -52,7 +52,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot

What the Mod Manager Should Look Like.

-![image](https://i.imgur.com/Si4oZ8w.png) +![image](https://i.imgur.com/N0WJ8Qn.png)

Using the KH2 Client

From 5a42c7067553995f9f630125a1860242452df4c7 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:00:47 +0100 Subject: [PATCH 23/57] Core: Fix worlds that rely on other worlds having their Entrances connected before connect_entrances, add unit test (#4530) * unit test that get all state is called with partial entrances before connect_entrances * fix the two worlds doing it * lol * unused import * Update test/general/test_entrances.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update test_entrances.py --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- test/general/test_entrances.py | 27 +++++++++++++++++++++++++++ worlds/alttp/Rules.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/test/general/test_entrances.py b/test/general/test_entrances.py index 72161dfbde..88362c8fa6 100644 --- a/test/general/test_entrances.py +++ b/test/general/test_entrances.py @@ -34,3 +34,30 @@ class TestBase(unittest.TestCase): self.assertEqual( original_entrances, step_entrances, f"{game_name} modified entrances during {step}" ) + + def test_all_state_before_connect_entrances(self): + """Before connect_entrances, Entrance objects may be unconnected. + Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during + connect_entrances.""" + + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances") + + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, ()) + + original_get_all_state = multiworld.get_all_state + + def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False): + self.assertTrue(allow_partial_entrances, ( + "Before the connect_entrances step finishes, other worlds might still have partial entrances. " + "As such, any call to get_all_state must use allow_partial_entrances = True." + )) + + return original_get_all_state(use_cache, allow_partial_entrances) + + multiworld.get_all_state = patched_get_all_state + + for step in gen_steps: + with self.subTest("Step", step=step): + call_all(multiworld, step) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 386e0b0e9e..f13178c6c5 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -1125,7 +1125,7 @@ def set_trock_key_rules(world, player): for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: set_rule(world.get_entrance(entrance, player), lambda state: False) - all_state = world.get_all_state(use_cache=False) + all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True) all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work all_state.stale[player] = True From fa2816822b46a770417b745e97d0f25c42b3a9ac Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:45:11 -0500 Subject: [PATCH 24/57] AHIT: Fix broken link in setup guide (#4524) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/ahit/docs/setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 23b3490707..167c6c2faa 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -21,7 +21,7 @@ 3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. - While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) + While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601). 4. Once the game finishes downloading, start it up. @@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked. ### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly -if you have too many save files. Delete them and it should fix the problem. \ No newline at end of file +if you have too many save files. Delete them and it should fix the problem. From bb0948154da8e3436ebd1ac9bbbc29ee230cc695 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 24 Jan 2025 12:42:31 -0500 Subject: [PATCH 25/57] TUNIC: Make the standard entrances get made with tuples instead of sets (#4546) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/tunic/regions.py | 46 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index 93ec5640e0..8f5df8896a 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -1,26 +1,24 @@ -from typing import Dict, Set - -tunic_regions: Dict[str, Set[str]] = { - "Menu": {"Overworld"}, - "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", +tunic_regions: dict[str, tuple[str]] = { + "Menu": ("Overworld",), + "Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", - "Spirit Arena"}, - "Overworld Holy Cross": set(), - "East Forest": set(), - "Dark Tomb": {"West Garden"}, - "Beneath the Well": set(), - "West Garden": set(), - "Ruined Atoll": {"Frog's Domain", "Library"}, - "Frog's Domain": set(), - "Library": set(), - "Eastern Vault Fortress": {"Beneath the Vault"}, - "Beneath the Vault": {"Eastern Vault Fortress"}, - "Quarry Back": {"Quarry"}, - "Quarry": {"Monastery", "Lower Quarry"}, - "Monastery": set(), - "Lower Quarry": {"Rooted Ziggurat"}, - "Rooted Ziggurat": set(), - "Swamp": {"Cathedral"}, - "Cathedral": set(), - "Spirit Arena": set() + "Spirit Arena"), + "Overworld Holy Cross": tuple(), + "East Forest": tuple(), + "Dark Tomb": ("West Garden",), + "Beneath the Well": tuple(), + "West Garden": tuple(), + "Ruined Atoll": ("Frog's Domain", "Library"), + "Frog's Domain": tuple(), + "Library": tuple(), + "Eastern Vault Fortress": ("Beneath the Vault",), + "Beneath the Vault": ("Eastern Vault Fortress",), + "Quarry Back": ("Quarry",), + "Quarry": ("Monastery", "Lower Quarry"), + "Monastery": tuple(), + "Lower Quarry": ("Rooted Ziggurat",), + "Rooted Ziggurat": tuple(), + "Swamp": ("Cathedral",), + "Cathedral": tuple(), + "Spirit Arena": tuple() } From 7474c273729f68bc9f791626999e180cacbc6b46 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 24 Jan 2025 13:52:12 -0500 Subject: [PATCH 26/57] Core: Add launch function to call launch_subprocess only if multiprocessing is actually necessary (#4237) * skips opening a subprocess if kivy (and thus the launcher gui) hasn't been loaded so stdin can function as expected on --nogui and similar * this exists lol * keep old function around and use new function for CC component * fix name=None typing --- worlds/LauncherComponents.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index d1b274c19a..41c83db419 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -87,7 +87,7 @@ class Component: processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: +def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: global processes import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) @@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = processes.add(process) +def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: + from Utils import is_kivy_running + if is_kivy_running(): + launch_subprocess(func, name, args) + else: + func(*args) + + class SuffixIdentifier: suffixes: Iterable[str] @@ -111,7 +119,7 @@ class SuffixIdentifier: def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) + launch(CommonClient.run_as_textclient, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: From 3d1d6908c8081f325659377e7d0dae4487badd07 Mon Sep 17 00:00:00 2001 From: Jasper den Brok Date: Fri, 24 Jan 2025 22:30:21 +0100 Subject: [PATCH 27/57] Pokemon Emerald: Add Free Fly Blacklist (#4165) Co-authored-by: Jasper den Brok --- worlds/pokemon_emerald/locations.py | 28 ++++++++++++++++------------ worlds/pokemon_emerald/options.py | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 473c189166..2bae8e00ed 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -33,6 +33,18 @@ VISITED_EVENT_NAME_TO_ID = { "EVENT_VISITED_SOUTHERN_ISLAND": 17, } +BLACKLIST_OPTION_TO_VISITED_EVENT = { + "Slateport City": "EVENT_VISITED_SLATEPORT_CITY", + "Mauville City": "EVENT_VISITED_MAUVILLE_CITY", + "Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN", + "Fallarbor Town": "EVENT_VISITED_FALLARBOR_TOWN", + "Lavaridge Town": "EVENT_VISITED_LAVARIDGE_TOWN", + "Fortree City": "EVENT_VISITED_FORTREE_CITY", + "Lilycove City": "EVENT_VISITED_LILYCOVE_CITY", + "Mossdeep City": "EVENT_VISITED_MOSSDEEP_CITY", + "Sootopolis City": "EVENT_VISITED_SOOTOPOLIS_CITY", + "Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY", +} class PokemonEmeraldLocation(Location): game: str = "Pokemon Emerald" @@ -129,18 +141,10 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None: # If not enabled, set it to Littleroot Town by default fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN" if world.options.free_fly_location: - fly_location_name = world.random.choice([ - "EVENT_VISITED_SLATEPORT_CITY", - "EVENT_VISITED_MAUVILLE_CITY", - "EVENT_VISITED_VERDANTURF_TOWN", - "EVENT_VISITED_FALLARBOR_TOWN", - "EVENT_VISITED_LAVARIDGE_TOWN", - "EVENT_VISITED_FORTREE_CITY", - "EVENT_VISITED_LILYCOVE_CITY", - "EVENT_VISITED_MOSSDEEP_CITY", - "EVENT_VISITED_SOOTOPOLIS_CITY", - "EVENT_VISITED_EVER_GRANDE_CITY", - ]) + blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value) + free_fly_locations = sorted(set(BLACKLIST_OPTION_TO_VISITED_EVENT.values()) - blacklisted_locations) + if free_fly_locations: + fly_location_name = world.random.choice(free_fly_locations) world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name] diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 8fcc74d1c3..cf0c692d06 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -725,6 +725,24 @@ class FreeFlyLocation(Toggle): """ display_name = "Free Fly Location" +class FreeFlyBlacklist(OptionSet): + """ + Disables specific locations as valid free fly locations. + Has no effect if Free Fly Location is disabled. + """ + display_name = "Free Fly Blacklist" + valid_keys = [ + "Slateport City", + "Mauville City", + "Verdanturf Town", + "Fallarbor Town", + "Lavaridge Town", + "Fortree City", + "Lilycove City", + "Mossdeep City", + "Sootopolis City", + "Ever Grande City", + ] class HmRequirements(Choice): """ @@ -876,6 +894,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions): extra_bumpy_slope: ExtraBumpySlope modify_118: ModifyRoute118 free_fly_location: FreeFlyLocation + free_fly_blacklist: FreeFlyBlacklist hm_requirements: HmRequirements turbo_a: TurboA From 3df2dbe051024df890f322280ee4373d4690c258 Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:55:49 -0500 Subject: [PATCH 28/57] TUNIC: Add ability shuffle information to spoiler log (#4498) --- worlds/tunic/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 087e17c3e4..ed2923037e 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, @@ -502,6 +502,13 @@ class TunicWorld(World): state.tunic_need_to_reset_combat_from_remove[self.player] = True return change + def write_spoiler_header(self, spoiler_handle: TextIO): + if self.options.hexagon_quest and self.options.ability_shuffling: + spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n") + for ability in self.ability_unlocks: + # Remove parentheses for better readability + spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n') + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) From ddf7fdccc718380e8611ab946ca5c529f897075b Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:57:23 -0500 Subject: [PATCH 29/57] TUNIC: Add Torch Item (#4538) Co-authored-by: Scipio Wright --- worlds/tunic/items.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 729bfd4411..846650c68f 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -48,6 +48,7 @@ item_table: Dict[str, TunicItemData] = { "Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), "Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful), "Dath Stone": TunicItemData(IC.useful, 1, 32), + "Torch": TunicItemData(IC.useful, 0, 156), "Hourglass": TunicItemData(IC.useful, 1, 33), "Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"), "Key": TunicItemData(IC.progression, 2, 35, "Keys"), From 513e361764aea8a04e56010c6c47ee4bb53f5303 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 24 Jan 2025 17:10:58 -0500 Subject: [PATCH 30/57] TUNIC: Fix UT create_item classification (#4514) Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com> --- worlds/tunic/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index ed2923037e..e86f731381 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -242,10 +242,18 @@ class TunicWorld(World): def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] - # if item_data.combat_ic is None, it'll take item_data.classification instead - itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None) + # evaluate alternate classifications based on options + # it'll choose whichever classification isn't None first in this if else tree + itemclass: ItemClassification = (classification + or (item_data.combat_ic if self.options.combat_logic else None) + or (ItemClassification.progression | ItemClassification.useful + if name == "Glass Cannon" and self.options.grass_randomizer + and not self.options.start_with_sword else None) + or (ItemClassification.progression | ItemClassification.useful + if name == "Shield" and self.options.ladder_storage + and not self.options.ladder_storage_without_items else None) or item_data.classification) - return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player) + return TunicItem(name, itemclass, self.item_name_to_id[name], self.player) def create_items(self) -> None: tunic_items: List[TunicItem] = [] @@ -278,8 +286,6 @@ class TunicWorld(World): if self.options.grass_randomizer: items_to_create["Grass"] = len(grass_location_table) - tunic_items.append(self.create_item("Glass Cannon", ItemClassification.progression)) - items_to_create["Glass Cannon"] = 0 for grass_location in excluded_grass_locations: self.get_location(grass_location).place_locked_item(self.create_item("Grass")) items_to_create["Grass"] -= len(excluded_grass_locations) @@ -351,11 +357,6 @@ class TunicWorld(World): tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful)) items_to_create[page] = 0 - # logically relevant if you have ladder storage enabled - if self.options.ladder_storage and not self.options.ladder_storage_without_items: - tunic_items.append(self.create_item("Shield", ItemClassification.progression)) - items_to_create["Shield"] = 0 - if self.options.maskless: tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 From cc770418f2d1d5c88ec08f30ac45c05ea704445c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:22:33 +0100 Subject: [PATCH 31/57] MultiServer: optimize PrintJSON for !release (#4545) * MultiServer: optimize PrintJSON for !release * MultiServer: safer comparison Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- MultiServer.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 653c2ecaab..9e0868b0f4 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1060,21 +1060,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], count_activity: bool = True): + slot_locations = ctx.locations[slot] new_locations = set(locations) - ctx.location_checks[team, slot] - new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata + new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata if new_locations: if count_activity: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) + + sortable: list[tuple[int, int, int, int]] = [] for location in new_locations: - item_id, target_player, flags = ctx.locations[slot][location] + # extract all fields to avoid runtime overhead in LocationStore + item_id, target_player, flags = slot_locations[location] + # sort/group by receiver and item + sortable.append((target_player, item_id, location, flags)) + + info_texts: list[dict[str, typing.Any]] = [] + for target_player, item_id, location, flags in sorted(sortable): new_item = NetworkItem(item_id, location, slot, flags) send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) - info_text = json_format_send_event(new_item, target_player) - ctx.broadcast_team(team, [info_text]) + if len(info_texts) >= 140: + # split into chunks that are close to compression window of 64K but not too big on the wire + # (roughly 1300-2600 bytes after compression depending on repetitiveness) + ctx.broadcast_team(team, info_texts) + info_texts.clear() + info_texts.append(json_format_send_event(new_item, target_player)) + ctx.broadcast_team(team, info_texts) + del info_texts + del sortable ctx.location_checks[team, slot] |= new_locations send_new_items(ctx) From 86641223c12852d998d45638ea664317d29f8e25 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 24 Jan 2025 18:35:54 -0500 Subject: [PATCH 32/57] Shivers: Stop using get_all_state cache to fix timing issue #4522 Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/shivers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 5c6203fd57..85f2cf1861 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -245,7 +245,7 @@ class ShiversWorld(World): storage_items += [self.create_item("Empty") for _ in range(3)] - state = self.multiworld.get_all_state(True) + state = self.multiworld.get_all_state(False) self.random.shuffle(storage_locs) self.random.shuffle(storage_items) From 1832bac1a3c0e9b046c67271ee09601b26b0fe94 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Jan 2025 06:35:42 -0800 Subject: [PATCH 33/57] BizHawkClient: Update README for `get_memory_size` (#4511) --- worlds/_bizhawk/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/worlds/_bizhawk/README.md b/worlds/_bizhawk/README.md index ddc70c3dd7..9058fc3061 100644 --- a/worlds/_bizhawk/README.md +++ b/worlds/_bizhawk/README.md @@ -55,6 +55,7 @@ async def lock(ctx) -> None async def unlock(ctx) -> None async def get_hash(ctx) -> str +async def get_memory_size(ctx, domain: str) -> int async def get_system(ctx) -> str async def get_cores(ctx) -> dict[str, str] async def ping(ctx) -> None @@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe associate the file extension with Archipelago. `validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is -running on a system you specified in your `system` class variable. In most cases, that will be a single system and you -can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this -ROM as yours, this is where you should do setup for things like `items_handling`. +running on a system you specified in your `system` class variable. Take extra care here, because your code will run +against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size +of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where +you should do setup for things like `items_handling`. `game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM. `BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do @@ -268,6 +270,8 @@ server connection before trying to interact with it. - By default, the player will be asked to provide their slot name after connecting to the server and validating, and that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to set it automatically based on data in the ROM or on your client instance. +- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a +smaller ROM size. - You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a subclass of `CommonContext` and its API. - You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at From 96b941ed35cb5d34a44e261be6e00a7efd38175d Mon Sep 17 00:00:00 2001 From: josephwhite Date: Sat, 25 Jan 2025 09:36:23 -0500 Subject: [PATCH 34/57] Super Mario 64: Add Star Costs to Spoiler (#4544) --- worlds/sm64ex/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index afa67f233c..d54e0fc64d 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -48,6 +48,17 @@ class SM64World(World): filler_count: int star_costs: typing.Dict[str, int] + # Spoiler specific variable(s) + star_costs_spoiler_key_maxlen = len(max([ + 'First Floor Big Star Door', + 'Basement Big Star Door', + 'Second Floor Big Star Door', + 'MIPS 1', + 'MIPS 2', + 'Endless Stairs', + ], key=len)) + + def generate_early(self): max_stars = 120 if (not self.options.enable_coin_stars): @@ -238,3 +249,19 @@ class SM64World(World): for location in region.locations: er_hint_data[location.address] = entrance_name hint_data[self.player] = er_hint_data + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + # Write calculated star costs to spoiler. + star_cost_spoiler_header = '\n\n' + self.player_name + ' Star Costs for Super Mario 64:\n\n' + spoiler_handle.write(star_cost_spoiler_header) + # - Reformat star costs dictionary in spoiler to be a bit more readable. + star_costs_spoiler = {} + star_costs_copy = self.star_costs.copy() + star_costs_spoiler['First Floor Big Star Door'] = star_costs_copy['FirstBowserDoorCost'] + star_costs_spoiler['Basement Big Star Door'] = star_costs_copy['BasementDoorCost'] + star_costs_spoiler['Second Floor Big Star Door'] = star_costs_copy['SecondFloorDoorCost'] + star_costs_spoiler['MIPS 1'] = star_costs_copy['MIPS1Cost'] + star_costs_spoiler['MIPS 2'] = star_costs_copy['MIPS2Cost'] + star_costs_spoiler['Endless Stairs'] = star_costs_copy['StarsToFinish'] + for star, cost in star_costs_spoiler.items(): + spoiler_handle.write(f"{star:{self.star_costs_spoiler_key_maxlen}s} = {cost}\n") From 90417e002292b3982f8dff68fae4270bdbd9db5c Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 26 Jan 2025 07:06:27 -0500 Subject: [PATCH 35/57] CommonClient: Expand on make_gui docstring (#4449) * adds docstring to make_gui describing what things you might want to change without dealing with kivy/kvui directly (there are better places to document those) * Update CommonClient.py Co-authored-by: Doug Hoskisson * Update CommonClient.py Co-authored-by: Doug Hoskisson --------- Co-authored-by: Doug Hoskisson --- CommonClient.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index f6b2623f8c..996ba33005 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -709,8 +709,16 @@ class CommonContext: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> typing.Type["kvui.GameManager"]: - """To return the Kivy App class needed for run_gui so it can be overridden before being built""" + def make_gui(self) -> "type[kvui.GameManager]": + """ + To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built + + Common changes are changing `base_title` to update the window title of the client and + updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger. + + ex. `logging_pairs.append(("Foo", "Bar"))` + will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")` + """ from kvui import GameManager class TextManager(GameManager): From 8622cb62040e1da2d1d3c66cb1563f76bddb57f9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 26 Jan 2025 22:14:39 +0100 Subject: [PATCH 36/57] Factorio: Inventory Spill Traps (#4457) --- worlds/factorio/Options.py | 7 ++++ worlds/factorio/__init__.py | 22 +++++------ worlds/factorio/data/mod/lib.lua | 37 +++++++++++++++++++ worlds/factorio/data/mod_template/control.lua | 5 +++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 0fa75e1b8b..4848cd9926 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -304,6 +304,11 @@ class EvolutionTrapIncrease(Range): range_end = 100 +class InventorySpillTrapCount(TrapCount): + """Trap items that when received trigger dropping your main inventory and trash inventory onto the ground.""" + display_name = "Inventory Spill Traps" + + class FactorioWorldGen(OptionDict): """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator, with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" @@ -484,6 +489,7 @@ class FactorioOptions(PerGameCommonOptions): artillery_traps: ArtilleryTrapCount atomic_rocket_traps: AtomicRocketTrapCount atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount + inventory_spill_traps: InventorySpillTrapCount attack_traps: AttackTrapCount evolution_traps: EvolutionTrapCount evolution_trap_increase: EvolutionTrapIncrease @@ -518,6 +524,7 @@ option_groups: list[OptionGroup] = [ ArtilleryTrapCount, AtomicRocketTrapCount, AtomicCliffRemoverTrapCount, + InventorySpillTrapCount, ], start_collapsed=True ), diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a2bc518ae3..ca9f12f1b2 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -78,6 +78,7 @@ all_items["Cluster Grenade Trap"] = factorio_base_id - 5 all_items["Artillery Trap"] = factorio_base_id - 6 all_items["Atomic Rocket Trap"] = factorio_base_id - 7 all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8 +all_items["Inventory Spill Trap"] = factorio_base_id - 9 class Factorio(World): @@ -112,6 +113,8 @@ class Factorio(World): science_locations: typing.List[FactorioScienceLocation] removed_technologies: typing.Set[str] settings: typing.ClassVar[FactorioSettings] + trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", + "Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill") def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) @@ -136,15 +139,11 @@ class Factorio(World): random = self.random nauvis = Region("Nauvis", player, self.multiworld) - location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.options.evolution_traps + \ - self.options.attack_traps + \ - self.options.teleport_traps + \ - self.options.grenade_traps + \ - self.options.cluster_grenade_traps + \ - self.options.atomic_rocket_traps + \ - self.options.atomic_cliff_remover_traps + \ - self.options.artillery_traps + location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + + for name in self.trap_names: + name = name.replace(" ", "_").lower()+"_traps" + location_count += getattr(self.options, name) location_pool = [] @@ -196,9 +195,8 @@ class Factorio(World): def create_items(self) -> None: self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() - traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket", - "Atomic Cliff Remover") - for trap_name in traps: + + for trap_name in self.trap_names: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in range(getattr(self.options, f"{trap_name.lower().replace(' ', '_')}_traps"))) diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 517a54e3d6..edec5b7acd 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -48,3 +48,40 @@ function fire_entity_at_entities(entity_name, entities, speed) target=target, speed=speed} end end + +function spill_character_inventory(character) + if not (character and character.valid) then + return false + end + + -- grab attrs once pre-loop + local position = character.position + local surface = character.surface + + local inventories_to_spill = { + defines.inventory.character_main, -- Main inventory + defines.inventory.character_trash, -- Logistic trash slots + } + + for _, inventory_type in pairs(inventories_to_spill) do + local inventory = character.get_inventory(inventory_type) + if inventory and inventory.valid then + -- Spill each item stack onto the ground + for i = 1, #inventory do + local stack = inventory[i] + if stack and stack.valid_for_read then + local spilled_items = surface.spill_item_stack{ + position = position, + stack = stack, + enable_looted = false, -- do not mark for auto-pickup + force = nil, -- do not mark for auto-deconstruction + allow_belts = true, -- do mark for putting it onto belts + } + if #spilled_items > 0 then + stack.clear() -- only delete if spilled successfully + end + end + end + end + end +end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 87669beaf1..07fd4c04af 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -750,6 +750,11 @@ end, fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1) end end, +["Inventory Spill Trap"] = function () + for _, player in ipairs(game.forces["player"].players) do + spill_character_inventory(player.character) + end +end, } commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) From 57a571cc110a0df310f0debc2a6fbbb9ea9304ca Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:52:02 -0600 Subject: [PATCH 37/57] KDL3: Fix world access on non-strict open world (#4543) * Update rules.py * lambda capture --- worlds/kdl3/rules.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/worlds/kdl3/rules.py b/worlds/kdl3/rules.py index a08e99257e..828740859e 100644 --- a/worlds/kdl3/rules.py +++ b/worlds/kdl3/rules.py @@ -206,19 +206,19 @@ def set_rules(world: "KDL3World") -> None: lambda state: can_reach_needle(state, world.player)) set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), lambda state: can_reach_ice(state, world.player) and - (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) - or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) - or can_reach_nago(state, world.player))) + (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) + or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) + or can_reach_nago(state, world.player))) set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), lambda state: can_reach_ice(state, world.player) and - (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) - or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) - or can_reach_nago(state, world.player))) + (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) + or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) + or can_reach_nago(state, world.player))) set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), lambda state: can_reach_ice(state, world.player) and - (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) - or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) - or can_reach_nago(state, world.player))) + (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) + or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) + or can_reach_nago(state, world.player))) set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), lambda state: can_reach_cutter(state, world.player)) @@ -248,9 +248,9 @@ def set_rules(world: "KDL3World") -> None: for i in range(12, 18): set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player), lambda state: can_reach_ice(state, world.player) and - (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) - or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) - or can_reach_nago(state, world.player))) + (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) + or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) + or can_reach_nago(state, world.player))) for i in range(21, 23): set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player), lambda state: can_reach_chuchu(state, world.player)) @@ -307,7 +307,7 @@ def set_rules(world: "KDL3World") -> None: lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) - and can_reach_burning(state, world.player)) + and can_reach_burning(state, world.player)) for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", @@ -329,6 +329,14 @@ def set_rules(world: "KDL3World") -> None: world.options.ow_boss_requirement.value, world.player_levels))) + if world.options.open_world: + for boss_flag, level in zip(["Level 1 Boss - Defeated", "Level 2 Boss - Defeated", "Level 3 Boss - Defeated", + "Level 4 Boss - Defeated", "Level 5 Boss - Defeated"], + location_name.level_names.keys()): + set_rule(world.get_location(boss_flag), + lambda state, lvl=level: state.has(f"{lvl} - Stage Completion", world.player, + world.options.ow_boss_requirement.value)) + set_rule(world.multiworld.get_entrance("To Level 6", world.player), lambda state: state.has("Heart Star", world.player, world.required_heart_stars)) From c43233120a828b4c89ee6c8ce1352c396fbe7266 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 27 Jan 2025 07:24:26 -0800 Subject: [PATCH 38/57] Pokemon Emerald: Clarify death link and start inventory descriptions (#4517) --- worlds/pokemon_emerald/__init__.py | 3 ++- worlds/pokemon_emerald/options.py | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 7b62b9ef73..50d6279179 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -22,7 +22,7 @@ from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, set_free_fly, set_legendary_cave_entrances) from .opponents import randomize_opponent_parties from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions, - RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement) + RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement, OPTION_GROUPS) from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets, randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters, randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters) @@ -63,6 +63,7 @@ class PokemonEmeraldWebWorld(WebWorld): ) tutorials = [setup_en, setup_es, setup_sv] + option_groups = OPTION_GROUPS class PokemonEmeraldSettings(settings.Group): diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index cf0c692d06..32644d52e0 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald from dataclasses import dataclass from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, - PerGameCommonOptions) + PerGameCommonOptions, OptionGroup, StartInventory) from .data import data @@ -803,6 +803,10 @@ class RandomizeFanfares(Toggle): display_name = "Randomize Fanfares" +class PokemonEmeraldDeathLink(DeathLink): + __doc__ = DeathLink.__doc__ + "\n\n In Pokemon Emerald, whiting out sends a death and receiving a death causes you to white out." + + class WonderTrading(DefaultOnToggle): """ Allows participation in wonder trading with other players in your current multiworld. Speak with the center receptionist on the second floor of any pokecenter. @@ -828,6 +832,14 @@ class EasterEgg(FreeText): default = "EMERALD SECRET" +class PokemonEmeraldStartInventory(StartInventory): + """ + Start with these items. + + They will be in your PC, which you can access from your home or a pokemon center. + """ + + @dataclass class PokemonEmeraldOptions(PerGameCommonOptions): goal: Goal @@ -904,7 +916,18 @@ class PokemonEmeraldOptions(PerGameCommonOptions): music: RandomizeMusic fanfares: RandomizeFanfares - death_link: DeathLink + death_link: PokemonEmeraldDeathLink enable_wonder_trading: WonderTrading easter_egg: EasterEgg + + start_inventory: PokemonEmeraldStartInventory + + +OPTION_GROUPS = [ + OptionGroup( + "Item & Location Options", [ + PokemonEmeraldStartInventory, + ], True, + ), +] From b570aa2ec6c811db280a835827aa3983f145a1a6 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 27 Jan 2025 07:25:31 -0800 Subject: [PATCH 39/57] Pokemon Emerald: Clean up free fly blacklist (#4552) --- worlds/pokemon_emerald/locations.py | 10 +++++++++- worlds/pokemon_emerald/options.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 2bae8e00ed..49ce147041 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -34,6 +34,11 @@ VISITED_EVENT_NAME_TO_ID = { } BLACKLIST_OPTION_TO_VISITED_EVENT = { + "Littleroot Town": "EVENT_VISITED_LITTLEROOT_TOWN", + "Oldale Town": "EVENT_VISITED_OLDALE_TOWN", + "Petalburg City": "EVENT_VISITED_PETALBURG_CITY", + "Rustboro City": "EVENT_VISITED_RUSTBORO_CITY", + "Dewford Town": "EVENT_VISITED_DEWFORD_TOWN", "Slateport City": "EVENT_VISITED_SLATEPORT_CITY", "Mauville City": "EVENT_VISITED_MAUVILLE_CITY", "Verdanturf Town": "EVENT_VISITED_VERDANTURF_TOWN", @@ -46,6 +51,9 @@ BLACKLIST_OPTION_TO_VISITED_EVENT = { "Ever Grande City": "EVENT_VISITED_EVER_GRANDE_CITY", } +VISITED_EVENTS = frozenset(BLACKLIST_OPTION_TO_VISITED_EVENT.values()) + + class PokemonEmeraldLocation(Location): game: str = "Pokemon Emerald" item_address: Optional[int] @@ -142,7 +150,7 @@ def set_free_fly(world: "PokemonEmeraldWorld") -> None: fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN" if world.options.free_fly_location: blacklisted_locations = set(BLACKLIST_OPTION_TO_VISITED_EVENT[city] for city in world.options.free_fly_blacklist.value) - free_fly_locations = sorted(set(BLACKLIST_OPTION_TO_VISITED_EVENT.values()) - blacklisted_locations) + free_fly_locations = sorted(VISITED_EVENTS - blacklisted_locations) if free_fly_locations: fly_location_name = world.random.choice(free_fly_locations) diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 32644d52e0..29929bd672 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -725,13 +725,20 @@ class FreeFlyLocation(Toggle): """ display_name = "Free Fly Location" + class FreeFlyBlacklist(OptionSet): """ Disables specific locations as valid free fly locations. + Has no effect if Free Fly Location is disabled. """ display_name = "Free Fly Blacklist" valid_keys = [ + "Littleroot Town", + "Oldale Town", + "Petalburg City", + "Rustboro City", + "Dewford Town", "Slateport City", "Mauville City", "Verdanturf Town", @@ -743,6 +750,14 @@ class FreeFlyBlacklist(OptionSet): "Sootopolis City", "Ever Grande City", ] + default = [ + "Littleroot Town", + "Oldale Town", + "Petalburg City", + "Rustboro City", + "Dewford Town", + ] + class HmRequirements(Choice): """ From 43874b1d28fa8d5a5bdc96d4408e303f57763ddd Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 27 Jan 2025 10:27:43 -0500 Subject: [PATCH 40/57] Noita: Add clarification to check option descriptions (#4553) --- worlds/noita/options.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/noita/options.py b/worlds/noita/options.py index 0fdd62365a..8a973a0d72 100644 --- a/worlds/noita/options.py +++ b/worlds/noita/options.py @@ -20,6 +20,8 @@ class PathOption(Choice): class HiddenChests(Range): """ Number of hidden chest checks added to the applicable biomes. + Note: The number of hidden chests that spawn per run in each biome varies. + You are expected do multiple runs to get all of your checks. """ display_name = "Hidden Chests per Biome" range_start = 0 @@ -30,6 +32,8 @@ class HiddenChests(Range): class PedestalChecks(Range): """ Number of checks that will spawn on pedestals in the applicable biomes. + Note: The number of pedestals that spawn per run in each biome varies. + You are expected do multiple runs to get all of your checks. """ display_name = "Pedestal Checks per Biome" range_start = 0 From 41055cd963c183244e262344e03d6ae6369fc52a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 27 Jan 2025 08:01:18 -0800 Subject: [PATCH 41/57] Pokemon Emerald: Update changelog (#4551) --- worlds/pokemon_emerald/CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0dd874b250..8d33d70900 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2.4.0 + +### Features + +- New option `free_fly_blacklist` limits which cities can show up as a free fly location. +- Spoiler log and hint text for maps where a species can be found now use human-friendly labels. +- Added many item and location groups based on item type, location type, and location geography. + +### Fixes + +- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize +event tickets. + # 2.3.0 ### Features From 8c5592e40684af4b9ac855e1a3b4b6e69622bffb Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:06:10 -0500 Subject: [PATCH 42/57] KH2: Fix determinism by using tuples instead of sets (#4548) --- worlds/kh2/Regions.py | 176 +++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index e6e8a7b2f6..72b3c95b09 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1032,99 +1032,99 @@ def connect_regions(self): multiworld = self.multiworld player = self.player # connecting every first visit to the GoA - KH2RegionConnections: typing.Dict[str, typing.Set[str]] = { - "Menu": {RegionName.GoA}, - RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht, + KH2RegionConnections: typing.Dict[str, typing.Tuple[str]] = { + "Menu": (RegionName.GoA,), + RegionName.GoA: (RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht, RegionName.LoD, RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb, RegionName.Dc, RegionName.Stt, RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1, RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, - RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne}, - RegionName.LoD: {RegionName.ShanYu}, - RegionName.ShanYu: {RegionName.LoD2}, - RegionName.LoD2: {RegionName.AnsemRiku}, - RegionName.AnsemRiku: {RegionName.StormRider}, - RegionName.StormRider: {RegionName.DataXigbar}, - RegionName.Ag: {RegionName.TwinLords}, - RegionName.TwinLords: {RegionName.Ag2}, - RegionName.Ag2: {RegionName.GenieJafar}, - RegionName.GenieJafar: {RegionName.DataLexaeus}, - RegionName.Dc: {RegionName.Tr}, - RegionName.Tr: {RegionName.OldPete}, - RegionName.OldPete: {RegionName.FuturePete}, - RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia}, - RegionName.Ha1: {RegionName.Ha2}, - RegionName.Ha2: {RegionName.Ha3}, - RegionName.Ha3: {RegionName.Ha4}, - RegionName.Ha4: {RegionName.Ha5}, - RegionName.Ha5: {RegionName.Ha6}, - RegionName.Pr: {RegionName.Barbosa}, - RegionName.Barbosa: {RegionName.Pr2}, - RegionName.Pr2: {RegionName.GrimReaper1}, - RegionName.GrimReaper1: {RegionName.GrimReaper2}, - RegionName.GrimReaper2: {RegionName.DataLuxord}, - RegionName.Oc: {RegionName.Cerberus}, - RegionName.Cerberus: {RegionName.OlympusPete}, - RegionName.OlympusPete: {RegionName.Hydra}, - RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2}, - RegionName.Oc2: {RegionName.Hades}, - RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion}, - RegionName.Oc2GofCup: {RegionName.HadesCups}, - RegionName.Bc: {RegionName.Thresholder}, - RegionName.Thresholder: {RegionName.Beast}, - RegionName.Beast: {RegionName.DarkThorn}, - RegionName.DarkThorn: {RegionName.Bc2}, - RegionName.Bc2: {RegionName.Xaldin}, - RegionName.Xaldin: {RegionName.DataXaldin}, - RegionName.Sp: {RegionName.HostileProgram}, - RegionName.HostileProgram: {RegionName.Sp2}, - RegionName.Sp2: {RegionName.Mcp}, - RegionName.Mcp: {RegionName.DataLarxene}, - RegionName.Ht: {RegionName.PrisonKeeper}, - RegionName.PrisonKeeper: {RegionName.OogieBoogie}, - RegionName.OogieBoogie: {RegionName.Ht2}, - RegionName.Ht2: {RegionName.Experiment}, - RegionName.Experiment: {RegionName.DataVexen}, - RegionName.Hb: {RegionName.Hb2}, - RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx}, - RegionName.HBDemyx: {RegionName.ThousandHeartless}, - RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi}, - RegionName.CoR: {RegionName.CorFirstFight}, - RegionName.CorFirstFight: {RegionName.CorSecondFight}, - RegionName.CorSecondFight: {RegionName.Transport}, - RegionName.Pl: {RegionName.Scar}, - RegionName.Scar: {RegionName.Pl2}, - RegionName.Pl2: {RegionName.GroundShaker}, - RegionName.GroundShaker: {RegionName.DataSaix}, - RegionName.Stt: {RegionName.TwilightThorn}, - RegionName.TwilightThorn: {RegionName.Axel1}, - RegionName.Axel1: {RegionName.Axel2}, - RegionName.Axel2: {RegionName.DataRoxas}, - RegionName.Tt: {RegionName.Tt2}, - RegionName.Tt2: {RegionName.Tt3}, - RegionName.Tt3: {RegionName.DataAxel}, - RegionName.Twtnw: {RegionName.Roxas}, - RegionName.Roxas: {RegionName.Xigbar}, - RegionName.Xigbar: {RegionName.Luxord}, - RegionName.Luxord: {RegionName.Saix}, - RegionName.Saix: {RegionName.Twtnw2}, - RegionName.Twtnw2: {RegionName.Xemnas}, - RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas}, - RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2}, - RegionName.ArmoredXemnas2: {RegionName.FinalXemnas}, - RegionName.LevelsVS1: {RegionName.LevelsVS3}, - RegionName.LevelsVS3: {RegionName.LevelsVS6}, - RegionName.LevelsVS6: {RegionName.LevelsVS9}, - RegionName.LevelsVS9: {RegionName.LevelsVS12}, - RegionName.LevelsVS12: {RegionName.LevelsVS15}, - RegionName.LevelsVS15: {RegionName.LevelsVS18}, - RegionName.LevelsVS18: {RegionName.LevelsVS21}, - RegionName.LevelsVS21: {RegionName.LevelsVS24}, - RegionName.LevelsVS24: {RegionName.LevelsVS26}, - RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo}, - RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree}, - RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour}, + RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne), + RegionName.LoD: (RegionName.ShanYu,), + RegionName.ShanYu: (RegionName.LoD2,), + RegionName.LoD2: (RegionName.AnsemRiku,), + RegionName.AnsemRiku: (RegionName.StormRider,), + RegionName.StormRider: (RegionName.DataXigbar,), + RegionName.Ag: (RegionName.TwinLords,), + RegionName.TwinLords: (RegionName.Ag2,), + RegionName.Ag2: (RegionName.GenieJafar,), + RegionName.GenieJafar: (RegionName.DataLexaeus,), + RegionName.Dc: (RegionName.Tr,), + RegionName.Tr: (RegionName.OldPete,), + RegionName.OldPete: (RegionName.FuturePete,), + RegionName.FuturePete: (RegionName.Terra, RegionName.DataMarluxia), + RegionName.Ha1: (RegionName.Ha2,), + RegionName.Ha2: (RegionName.Ha3,), + RegionName.Ha3: (RegionName.Ha4,), + RegionName.Ha4: (RegionName.Ha5,), + RegionName.Ha5: (RegionName.Ha6,), + RegionName.Pr: (RegionName.Barbosa,), + RegionName.Barbosa: (RegionName.Pr2,), + RegionName.Pr2: (RegionName.GrimReaper1,), + RegionName.GrimReaper1: (RegionName.GrimReaper2,), + RegionName.GrimReaper2: (RegionName.DataLuxord,), + RegionName.Oc: (RegionName.Cerberus,), + RegionName.Cerberus: (RegionName.OlympusPete,), + RegionName.OlympusPete: (RegionName.Hydra,), + RegionName.Hydra: (RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2), + RegionName.Oc2: (RegionName.Hades,), + RegionName.Hades: (RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion), + RegionName.Oc2GofCup: (RegionName.HadesCups,), + RegionName.Bc: (RegionName.Thresholder,), + RegionName.Thresholder: (RegionName.Beast,), + RegionName.Beast: (RegionName.DarkThorn,), + RegionName.DarkThorn: (RegionName.Bc2,), + RegionName.Bc2: (RegionName.Xaldin,), + RegionName.Xaldin: (RegionName.DataXaldin,), + RegionName.Sp: (RegionName.HostileProgram,), + RegionName.HostileProgram: (RegionName.Sp2,), + RegionName.Sp2: (RegionName.Mcp,), + RegionName.Mcp: (RegionName.DataLarxene,), + RegionName.Ht: (RegionName.PrisonKeeper,), + RegionName.PrisonKeeper: (RegionName.OogieBoogie,), + RegionName.OogieBoogie: (RegionName.Ht2,), + RegionName.Ht2: (RegionName.Experiment,), + RegionName.Experiment: (RegionName.DataVexen,), + RegionName.Hb: (RegionName.Hb2,), + RegionName.Hb2: (RegionName.CoR, RegionName.HBDemyx), + RegionName.HBDemyx: (RegionName.ThousandHeartless,), + RegionName.ThousandHeartless: (RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi), + RegionName.CoR: (RegionName.CorFirstFight,), + RegionName.CorFirstFight: (RegionName.CorSecondFight,), + RegionName.CorSecondFight: (RegionName.Transport,), + RegionName.Pl: (RegionName.Scar,), + RegionName.Scar: (RegionName.Pl2,), + RegionName.Pl2: (RegionName.GroundShaker,), + RegionName.GroundShaker: (RegionName.DataSaix,), + RegionName.Stt: (RegionName.TwilightThorn,), + RegionName.TwilightThorn: (RegionName.Axel1,), + RegionName.Axel1: (RegionName.Axel2,), + RegionName.Axel2: (RegionName.DataRoxas,), + RegionName.Tt: (RegionName.Tt2,), + RegionName.Tt2: (RegionName.Tt3,), + RegionName.Tt3: (RegionName.DataAxel,), + RegionName.Twtnw: (RegionName.Roxas,), + RegionName.Roxas: (RegionName.Xigbar,), + RegionName.Xigbar: (RegionName.Luxord,), + RegionName.Luxord: (RegionName.Saix,), + RegionName.Saix: (RegionName.Twtnw2,), + RegionName.Twtnw2: (RegionName.Xemnas,), + RegionName.Xemnas: (RegionName.ArmoredXemnas, RegionName.DataXemnas), + RegionName.ArmoredXemnas: (RegionName.ArmoredXemnas2,), + RegionName.ArmoredXemnas2: (RegionName.FinalXemnas,), + RegionName.LevelsVS1: (RegionName.LevelsVS3,), + RegionName.LevelsVS3: (RegionName.LevelsVS6,), + RegionName.LevelsVS6: (RegionName.LevelsVS9,), + RegionName.LevelsVS9: (RegionName.LevelsVS12,), + RegionName.LevelsVS12: (RegionName.LevelsVS15,), + RegionName.LevelsVS15: (RegionName.LevelsVS18,), + RegionName.LevelsVS18: (RegionName.LevelsVS21,), + RegionName.LevelsVS21: (RegionName.LevelsVS24,), + RegionName.LevelsVS24: (RegionName.LevelsVS26,), + RegionName.AtlanticaSongOne: (RegionName.AtlanticaSongTwo,), + RegionName.AtlanticaSongTwo: (RegionName.AtlanticaSongThree,), + RegionName.AtlanticaSongThree: (RegionName.AtlanticaSongFour,), } for source, target in KH2RegionConnections.items(): From a53bcb4697f1a077075cc603ad4588a693c3b23d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:13:10 +0100 Subject: [PATCH 43/57] KH2: Use int(..., 0) in Client #4562 --- worlds/kh2/Client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 0254d46e93..a21c8c7c55 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -836,12 +836,12 @@ class KH2Context(CommonContext): if self.mem_json: for key in self.mem_json.keys(): - if self.kh2_read_string(eval(self.mem_json[key]["GameVersionCheck"]), 4) == "KH2J": - self.Now = eval(self.mem_json[key]["Now"]) - self.Save=eval(self.mem_json[key]["Save"]) - self.Slot1 = eval(self.mem_json[key]["Slot1"]) - self.Journal = eval(self.mem_json[key]["Journal"]) - self.Shop = eval(self.mem_json[key]["Shop"]) + if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J": + self.Now = int(self.mem_json[key]["Now"], 0) + self.Save = int(self.mem_json[key]["Save"], 0) + self.Slot1 = int(self.mem_json[key]["Slot1"], 0) + self.Journal = int(self.mem_json[key]["Journal"], 0) + self.Shop = int(self.mem_json[key]["Shop"], 0) self.kh2_game_version = key if self.kh2_game_version is not None: From 9466d5274e5759d0081f02e0aad9829dd1f1dbd3 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:45:28 -0600 Subject: [PATCH 44/57] MM2: fix plando and weakness special cases (#4561) --- worlds/mm2/options.py | 2 +- worlds/mm2/rules.py | 61 ++++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/worlds/mm2/options.py b/worlds/mm2/options.py index 2d90395cac..f333348982 100644 --- a/worlds/mm2/options.py +++ b/worlds/mm2/options.py @@ -175,7 +175,7 @@ class WeaknessPlando(OptionDict): display_name = "Plando Weaknesses" schema = Schema({ Optional(And(str, Use(str.title), lambda s: s in bosses)): { - And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14)) + And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 15)) } }) default = {} diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index 7e2ce1f3c7..7e03edf3a2 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -135,41 +135,47 @@ def set_rules(world: "MM2World") -> None: world.weapon_damage[weapon][i] = 0 for p_boss in world.options.plando_weakness: + boss = bosses[p_boss] for p_weapon in world.options.plando_weakness[p_boss]: - if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \ - and not any(w != p_weapon - and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w] - for w in world.weapon_damage): + weapon = weapons_to_id[p_weapon] + if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[weapon] \ + and not any(w != weapon + and world.weapon_damage[w][boss] >= minimum_weakness_requirement[w] + for w in world.weapon_damage): # we need to replace this weakness - weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon]) - world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness] - world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ - = world.options.plando_weakness[p_boss][p_weapon] + weakness = world.random.choice([key for key in world.weapon_damage if key != weapon]) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + world.weapon_damage[weapon][boss] = world.options.plando_weakness[p_boss][p_weapon] # handle special cases for boss in range(14): for weapon in (1, 2, 3, 6, 8): if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and - not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[i] for i in range(9) if i != weapon)): # Weapon does not have enough possible ammo to kill the boss, raise the damage - if boss == 9: - if weapon in (1, 6): - # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - elif boss == 11: - if weapon == 1: - # Atomic Fire cannot be Boobeam Trap's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - else: - world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + + for weapon in (1, 6): + if (world.weapon_damage[weapon][9] >= minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][9] >= minimum_weakness_requirement[i] + for i in range(9) if i not in (1, 6))): + # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness + world.weapon_damage[weapon][9] = 0 + weakness = world.random.choice((2, 3, 4, 5, 7, 8)) + world.weapon_damage[weakness][9] = minimum_weakness_requirement[weakness] + + if (world.weapon_damage[1][11] >= minimum_weakness_requirement[1] and + not any(world.weapon_damage[i][11] >= minimum_weakness_requirement[i] + for i in range(9) if i != 1)): + # Atomic Fire cannot be Boobeam Trap's only weakness + world.weapon_damage[1][11] = 0 + weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) + world.weapon_damage[weakness][11] = minimum_weakness_requirement[weakness] if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: - world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] + world.weapon_damage[0][world.options.starting_robot_master.value] = \ + weapon_damage[0][world.options.starting_robot_master.value] # final special case # There's a vanilla crash if Time Stopper kills Wily phase 1 @@ -218,9 +224,10 @@ def set_rules(world: "MM2World") -> None: # we are out of weapons that can actually damage the boss # so find the weapon that has the most uses, and apply that as an additional weakness # it should be impossible to be out of energy, simply because even if every boss took 1 from - # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should - # be able to cover - wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight + # Quick Boomerang and no other, it would only be 28 off from defeating all 9, + # which Metal Blade should be able to cover + wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) + for weapon in weapon_weight if weapon != 0 and (weapon != 8 or boss != 12)) # Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] From 1ebc9e2ec03de4dc3c18af6b0d9e82655614ff81 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 28 Jan 2025 17:19:20 -0500 Subject: [PATCH 45/57] Stardew Valley: Tests: Restructure the tests that validate Mods + ER together, improved performance (#4557) * - Unrolled and improved the structure of the test for Mods + ER, to improve total performance and performance on individual tests for threading purposes * Use | instead of Union[] Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> * - Remove unused using --------- Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> --- worlds/stardew_valley/test/mods/TestMods.py | 65 +++++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 89f82870e4..02592cc383 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -7,7 +7,9 @@ from ..assertion import ModAssertMixin, WorldAssertMixin from ... import items, Group, ItemClassification, create_content from ... import options from ...items import items_by_group +from ...mods.mod_data import ModNames from ...options import SkillProgression, Walnutsanity +from ...options.options import all_mods from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions @@ -20,17 +22,58 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) - def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self): - for option in options.EntranceRandomization.options: - for mod in options.Mods.valid_keys: - world_options = { - options.EntranceRandomization: options.EntranceRandomization.options[option], - options.Mods: mod, - options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false - } - with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): - self.assert_basic_checks(multi_world) - self.assert_stray_mod_items(mod, multi_world) + # The following tests validate that ER still generates winnable and logically-sane games with given mods. + # Mods that do not interact with entrances are skipped + # Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others + def test_deepwoods_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.deepwoods, options.EntranceRandomization.option_buildings) + + def test_juna_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.juna, options.EntranceRandomization.option_buildings) + + def test_jasper_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.jasper, options.EntranceRandomization.option_buildings) + + def test_alec_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.alec, options.EntranceRandomization.option_buildings) + + def test_yoba_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.yoba, options.EntranceRandomization.option_buildings) + + def test_eugene_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.eugene, options.EntranceRandomization.option_buildings) + + def test_ayeisha_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.ayeisha, options.EntranceRandomization.option_buildings) + + def test_riley_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.riley, options.EntranceRandomization.option_buildings) + + def test_sve_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.sve, options.EntranceRandomization.option_buildings) + + def test_alecto_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.alecto, options.EntranceRandomization.option_buildings) + + def test_lacey_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.lacey, options.EntranceRandomization.option_buildings) + + def test_boarding_house_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(ModNames.boarding_house, options.EntranceRandomization.option_buildings) + + def test_all_mods_entrance_randomization_buildings(self): + self.perform_basic_checks_on_mod_with_er(all_mods, options.EntranceRandomization.option_buildings) + + def perform_basic_checks_on_mod_with_er(self, mods: str | set[str], er_option: int) -> None: + if isinstance(mods, str): + mods = {mods} + world_options = { + options.EntranceRandomization: er_option, + options.Mods: frozenset(mods), + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false + } + with self.solo_world_sub_test(f"entrance_randomization: {er_option}, Mods: {mods}", world_options) as (multi_world, _): + self.assert_basic_checks(multi_world) def test_allsanity_all_mods_when_generate_then_basic_checks(self): with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _): From 41898ed6403fa62487c51880792a648d1f4d246b Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:42:46 +0100 Subject: [PATCH 46/57] MultiServer: implement NoText and deprecate uncompressed Websocket connections (#4540) * MultiServer: add NoText tag and handling * MultiServer: deprecate and warn for uncompressed connections * MultiServer: fix missing space in no compression warning --- MultiServer.py | 51 +++++++++++++++++++++++++++++----------- NetUtils.py | 5 ++-- docs/network protocol.md | 4 ++++ 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 9e0868b0f4..51b72c93ad 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -28,9 +28,11 @@ ModuleUpdate.update() if typing.TYPE_CHECKING: import ssl + from NetUtils import ServerConnection -import websockets import colorama +import websockets +from websockets.extensions.permessage_deflate import PerMessageDeflate try: # ponyorm is a requirement for webhost, not default server, so may not be importable from pony.orm.dbapiprovider import OperationalError @@ -119,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int: class Client(Endpoint): version = Version(0, 0, 0) - tags: typing.List[str] = [] + tags: typing.List[str] remote_items: bool remote_start_inventory: bool no_items: bool no_locations: bool + no_text: bool - def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context): + def __init__(self, socket: "ServerConnection", ctx: Context) -> None: super().__init__(socket) self.auth = False self.team = None @@ -175,6 +178,7 @@ class Context: "compatibility": int} # team -> slot id -> list of clients authenticated to slot. clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] + endpoints: list[Client] locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]] hints_used: typing.Dict[typing.Tuple[int, int], int] @@ -364,18 +368,28 @@ class Context: return True def broadcast_all(self, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in self.endpoints + if endpoint.auth and not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast_text_all(self, text: str, additional_arguments: dict = {}): self.logger.info("Notice (all): %s" % text) self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) def broadcast_team(self, team: int, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values())) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in itertools.chain.from_iterable(self.clients[team].values()) + if not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]): msgs = self.dumper(msgs) @@ -389,13 +403,13 @@ class Context: await on_client_disconnected(self, endpoint) def notify_client(self, client: Client, text: str, additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}])) def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} @@ -760,7 +774,7 @@ class Context: self.on_new_hint(team, slot) for slot, hint_data in concerns.items(): if recipients is None or slot in recipients: - clients = self.clients[team].get(slot) + clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, [])) if not clients: continue client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)] @@ -819,7 +833,7 @@ def update_aliases(ctx: Context, team: int): async_start(ctx.send_encoded_msgs(client, cmd)) -async def server(websocket, path: str = "/", ctx: Context = None): +async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None: client = Client(websocket, ctx) ctx.endpoints.append(client) @@ -910,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client): "If your client supports it, " "you may have additional local commands you can list with /help.", {"type": "Tutorial"}) + if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions): + ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! " + "It may stop working in the future. If you are a player, please report this to the " + "client's developer.") ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -1803,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): ctx.clients[team][slot].append(client) client.version = args['version'] client.tags = args['tags'] - client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags + # set NoText for old PopTracker clients that predate the tag to save traffic + client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1)) connected_packet = { "cmd": "Connected", "team": client.team, "slot": client.slot, @@ -1876,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): client.tags = args["tags"] if set(old_tags) != set(client.tags): client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_text = "NoText" in client.tags or ( + "PopTracker" in client.tags and client.version < (0, 5, 1) + ) ctx.broadcast_text_all( f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"from {old_tags} to {client.tags}.", diff --git a/NetUtils.py b/NetUtils.py index d58bbe81e3..5bcc583c53 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -5,7 +5,8 @@ import enum import warnings from json import JSONEncoder, JSONDecoder -import websockets +if typing.TYPE_CHECKING: + from websockets import WebSocketServerProtocol as ServerConnection from Utils import ByValue, Version @@ -151,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode class Endpoint: - socket: websockets.WebSocketServerProtocol + socket: "ServerConnection" def __init__(self, socket): self.socket = socket diff --git a/docs/network protocol.md b/docs/network protocol.md index e32c266ffb..2eb3b0d6f3 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -47,6 +47,9 @@ Packets are simple JSON lists in which any number of ordered network commands ca An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example. +Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop +working in the future. + Example: ```javascript [{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }] @@ -745,6 +748,7 @@ Tags are represented as a list of strings, the common client tags follow: | HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² | | Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² | | TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² | +| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. | ¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\ ²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped. From 738c21c625f673caac2d10c173688a10f23c86a1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:52:01 +0100 Subject: [PATCH 47/57] Tests: massively improve the memory leak test performance (#4568) * Tests: massively improve the memory leak test performance With the growing number of worlds, GC becomes the bottleneck and slows down the test. * Tests: fix typing in general/test_memory --- test/general/test_memory.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/general/test_memory.py b/test/general/test_memory.py index 987d19acf3..a4b2f1bd25 100644 --- a/test/general/test_memory.py +++ b/test/general/test_memory.py @@ -1,5 +1,6 @@ import unittest +from BaseClasses import MultiWorld from worlds.AutoWorld import AutoWorldRegister from . import setup_solo_multiworld @@ -9,8 +10,12 @@ class TestWorldMemory(unittest.TestCase): """Tests that worlds don't leak references to MultiWorld or themselves with default options.""" import gc import weakref + refs: dict[str, weakref.ReferenceType[MultiWorld]] = {} for game_name, world_type in AutoWorldRegister.world_types.items(): - with self.subTest("Game", game_name=game_name): + with self.subTest("Game creation", game_name=game_name): weak = weakref.ref(setup_solo_multiworld(world_type)) - gc.collect() + refs[game_name] = weak + gc.collect() + for game_name, weak in refs.items(): + with self.subTest("Game cleanup", game_name=game_name): self.assertFalse(weak(), "World leaked a reference") From 57afdfda6f6535bc592581d70d97a3f12977b5a1 Mon Sep 17 00:00:00 2001 From: Felix R <50271878+FelicitusNeko@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:03:37 -0400 Subject: [PATCH 48/57] meritous: move completion_condition to set_rules (#4567) --- worlds/meritous/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 7a21b19ef2..2263478ff5 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -136,6 +136,12 @@ class MeritousWorld(World): def set_rules(self): set_rules(self.multiworld, self.player) + if self.goal == 0: + self.multiworld.completion_condition[self.player] = lambda state: state.has_any( + ["Victory", "Full Victory"], self.player) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has( + "Full Victory", self.player) def generate_basic(self): self.multiworld.get_location("Place of Power", self.player).place_locked_item( @@ -166,13 +172,6 @@ class MeritousWorld(World): self.multiworld.get_location(boss, self.player).place_locked_item( self.create_item("Evolution Trap")) - if self.goal == 0: - self.multiworld.completion_condition[self.player] = lambda state: state.has_any( - ["Victory", "Full Victory"], self.player) - else: - self.multiworld.completion_condition[self.player] = lambda state: state.has( - "Full Victory", self.player) - def fill_slot_data(self) -> dict: return { "goal": self.goal, From b8666b25625b0cd2341b9747bc126127cd26022e Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:56:50 -0500 Subject: [PATCH 49/57] Stardew Valley: Remove weird magic trap test? (#4570) --- worlds/stardew_valley/test/mods/TestMods.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 02592cc383..1dd2ab4902 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -1,12 +1,10 @@ import random from BaseClasses import get_seed -from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \ - fill_dataclass_with_default +from .. import SVTestBase, SVTestCase, allsanity_mods_6_x_x, fill_dataclass_with_default from ..assertion import ModAssertMixin, WorldAssertMixin from ... import items, Group, ItemClassification, create_content from ... import options -from ...items import items_by_group from ...mods.mod_data import ModNames from ...options import SkillProgression, Walnutsanity from ...options.options import all_mods @@ -190,19 +188,3 @@ class TestModEntranceRando(SVTestCase): self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()), f"Connections are duplicated in randomization.") - - -class TestModTraps(SVTestCase): - def test_given_traps_when_generate_then_all_traps_in_pool(self): - for value in options.TrapItems.options: - if value == "no_traps": - continue - - world_options = allsanity_no_mods_6_x_x() - world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"}) - with solo_multiworld(world_options) as (multi_world, _): - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) From 8e14e463e41945378090da40ab620698baf6d8cc Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 30 Jan 2025 03:05:51 -0500 Subject: [PATCH 50/57] Stardew Valley: Radioactive slot machine should be a ginger island check (#4578) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 43883b86f8..66a9157b34 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2938,7 +2938,7 @@ id,region,name,tags,mod_name 7440,Farm,Craft Copper Slot Machine,"CRAFTSANITY",Luck Skill 7441,Farm,Craft Gold Slot Machine,"CRAFTSANITY",Luck Skill 7442,Farm,Craft Iridium Slot Machine,"CRAFTSANITY",Luck Skill -7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY",Luck Skill +7443,Farm,Craft Radioactive Slot Machine,"CRAFTSANITY,GINGER_ISLAND",Luck Skill 7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic 7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic 7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded From 1fe8024b438dd56bd20e6b26c8d14fe1e1fbd0b4 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 30 Jan 2025 03:19:06 -0500 Subject: [PATCH 51/57] Stardew valley: Add Mod Recipes tests (#4580) * `- Add Craftsanity Mod tests * - Add the same test for cooking --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- .../stardew_valley/test/mods/TestModsFill.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 worlds/stardew_valley/test/mods/TestModsFill.py diff --git a/worlds/stardew_valley/test/mods/TestModsFill.py b/worlds/stardew_valley/test/mods/TestModsFill.py new file mode 100644 index 0000000000..a140f5abae --- /dev/null +++ b/worlds/stardew_valley/test/mods/TestModsFill.py @@ -0,0 +1,28 @@ +from .. import SVTestBase +from ... import options + + +class TestNoGingerIslandCraftingRecipesAreRequired(SVTestBase): + options = { + options.Goal.internal_name: options.Goal.option_craft_master, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) + } + + @property + def run_default_tests(self) -> bool: + return True + + +class TestNoGingerIslandCookingRecipesAreRequired(SVTestBase): + options = { + options.Goal.internal_name: options.Goal.option_gourmet_chef, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) + } + + @property + def run_default_tests(self) -> bool: + return True From 67e8877143aecf3587f7b60bdb34659de1500e0d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:38:17 +0100 Subject: [PATCH 52/57] Docs: fix lower limit of valid IDs in network protocol.md (#4579) --- docs/network protocol.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 2eb3b0d6f3..e5d3b7e6c2 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -533,9 +533,9 @@ In JSON this may look like: {"item": 3, "location": 3, "player": 3, "flags": 0} ] ``` -`item` is the item id of the item. Item ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. +`item` is the item id of the item. Item ids are only supported in the range of [-253 + 1, 253 - 1], with anything ≤ 0 reserved for Archipelago use. -`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. +`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253 + 1, 253 - 1], with anything ≤ 0 reserved for Archipelago use. `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item From 445c9b22d6cfb9b8ff4e76b91995d08abf154ff5 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 31 Jan 2025 20:11:04 -0500 Subject: [PATCH 53/57] Settings: Handle empty Groups (#4576) * export empty groups as an empty dict instead of crashing * Update settings.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * check instance values from self as well * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 12dace632c..cc808c2732 100644 --- a/settings.py +++ b/settings.py @@ -109,7 +109,7 @@ class Group: def get_type_hints(cls) -> Dict[str, Any]: """Returns resolved type hints for the class""" if cls._type_cache is None: - if not isinstance(next(iter(cls.__annotations__.values())), str): + if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str): # non-str: assume already resolved cls._type_cache = cls.__annotations__ else: @@ -270,11 +270,15 @@ class Group: # fetch class to avoid going through getattr cls = self.__class__ type_hints = cls.get_type_hints() + entries = [e for e in self] + if not entries: + # write empty dict for empty Group with no instance values + cls._dump_value({}, f, indent=" " * level) # validate group for name in cls.__annotations__.keys(): assert hasattr(cls, name), f"{cls}.{name} is missing a default value" # dump ordered members - for name in self: + for name in entries: attr = cast(object, getattr(self, name)) attr_cls = type_hints[name] if name in type_hints else attr.__class__ attr_cls_origin = typing.get_origin(attr_cls) From d1167027f4d723856e555a8c9ca7cfe7ce8dde4f Mon Sep 17 00:00:00 2001 From: Jarno Date: Sat, 1 Feb 2025 02:26:59 +0100 Subject: [PATCH 54/57] Core: Make csv options output ignore hidden options (#4539) * Core: Make csv options output ignore hidden options * Update Options.py Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index d9122d444c..49e82069ee 100644 --- a/Options.py +++ b/Options.py @@ -1582,7 +1582,7 @@ def dump_player_options(multiworld: MultiWorld) -> None: } output.append(player_output) for option_key, option in world.options_dataclass.type_hints.items(): - if issubclass(Removed, option): + if option.visibility == Visibility.none: continue display_name = getattr(option, "display_name", option_key) player_output[display_name] = getattr(world.options, option_key).current_option_name From b7b78dead3bf181545352df1b0e3229fc592b9f2 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Sat, 1 Feb 2025 22:03:49 +0100 Subject: [PATCH 55/57] LADX: Fix generation error on minimal accessibility (#4281) * [LADX] Fix minimal accessibility * allow_partial for minimal accessibility * create the correct partial_all_state * skip our prefills rather than removing after * dont rebuild our prefill list --------- Co-authored-by: threeandthreee --- worlds/ladx/__init__.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 7b1a35666a..a887638e37 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -9,7 +9,7 @@ import re import bsdiff4 import settings -from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World from .Common import * @@ -315,8 +315,6 @@ class LinksAwakeningWorld(World): # Set up filter rules - # The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons - all_dungeon_items_to_fill = list(self.prefill_own_dungeons) # set containing the list of all possible dungeon locations for the player all_dungeon_locs = set() @@ -327,9 +325,6 @@ class LinksAwakeningWorld(World): for item in self.prefill_original_dungeon[dungeon_index]: allowed_locations_by_item[item] = locs - # put the items for this dungeon in the list to fill - all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index]) - # ...and gather the list of all dungeon locations all_dungeon_locs |= locs # ...also set the rules for the dungeon @@ -369,16 +364,27 @@ class LinksAwakeningWorld(World): if allowed_locations_by_item[item] is all_dungeon_locs: i += 3 return i + all_dungeon_items_to_fill = self.get_pre_fill_items() all_dungeon_items_to_fill.sort(key=priority) # Set up state - all_state = self.multiworld.get_all_state(use_cache=False) - # Remove dungeon items we are about to put in from the state so that we don't double count - for item in all_dungeon_items_to_fill: - all_state.remove(item) + partial_all_state = CollectionState(self.multiworld) + # Collect every item from the item pool and every pre-fill item like MultiWorld.get_all_state, except not our own pre-fill items. + for item in self.multiworld.itempool: + partial_all_state.collect(item, prevent_sweep=True) + for player in self.multiworld.player_ids: + if player == self.player: + # Don't collect the items we're about to place. + continue + subworld = self.multiworld.worlds[player] + for item in subworld.get_pre_fill_items(): + partial_all_state.collect(item, prevent_sweep=True) + + # Sweep to pick up already placed items that are reachable with everything but the dungeon items. + partial_all_state.sweep_for_advancements() - # Finally, fill! - fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) + fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) + name_cache = {} # Tries to associate an icon from another game with an icon we have From 051518e72aaed0b49d43fe80c01129fd52aca729 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:07:08 -0500 Subject: [PATCH 56/57] Stardew Valley: Fix unresolved reference warning and unused imports (#4360) * fix unresolved reference warning and unused imports * revert stuff * just a commit to rerun the tests cuz messenger fail --- worlds/stardew_valley/__init__.py | 6 +- worlds/stardew_valley/bundles/bundle_room.py | 2 +- worlds/stardew_valley/content/mods/sve.py | 19 +- .../content/vanilla/qi_board.py | 1 - worlds/stardew_valley/data/bundle_data.py | 4 +- worlds/stardew_valley/data/craftable_data.py | 11 +- worlds/stardew_valley/data/recipe_data.py | 42 +- worlds/stardew_valley/data/recipe_source.py | 2 +- worlds/stardew_valley/logic/ability_logic.py | 10 +- worlds/stardew_valley/logic/action_logic.py | 1 - worlds/stardew_valley/logic/skill_logic.py | 8 +- .../stardew_valley/mods/logic/item_logic.py | 7 +- .../stardew_valley/mods/logic/quests_logic.py | 5 +- worlds/stardew_valley/regions.py | 621 +++++++++--------- worlds/stardew_valley/scripts/update_data.py | 8 +- worlds/stardew_valley/stardew_rule/base.py | 4 +- .../test/TestMultiplePlayers.py | 2 - .../stardew_valley/test/TestWalnutsanity.py | 12 +- .../stardew_valley/test/rules/TestFishing.py | 3 +- 19 files changed, 395 insertions(+), 373 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index ef842263ad..e2d49e64ae 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,6 +1,6 @@ import logging from random import Random -from typing import Dict, Any, Iterable, Optional, List, TextIO +from typing import Dict, Any, Iterable, Optional, List, TextIO, cast from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from Options import PerGameCommonOptions @@ -124,7 +124,7 @@ class StardewValleyWorld(World): self.options) def add_location(name: str, code: Optional[int], region: str): - region = world_regions[region] + region: Region = world_regions[region] location = StardewLocation(self.player, name, code, region) region.locations.append(location) @@ -314,9 +314,9 @@ class StardewValleyWorld(World): include_traps = True exclude_island = False for player in link_group["players"]: - player_options = self.multiworld.worlds[player].options if self.multiworld.game[player] != self.game: continue + player_options = cast(StardewValleyOptions, self.multiworld.worlds[player].options) if player_options.trap_items == TrapItems.option_no_traps: include_traps = False if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true: diff --git a/worlds/stardew_valley/bundles/bundle_room.py b/worlds/stardew_valley/bundles/bundle_room.py index 8068ff17ac..225fb4feab 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -4,7 +4,7 @@ from typing import List from .bundle import Bundle, BundleTemplate from ..content import StardewContent -from ..options import BundlePrice, StardewValleyOptions +from ..options import StardewValleyOptions @dataclass diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py index a68d4ae9c0..12b3e3558a 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -10,15 +10,14 @@ from ...data.shop import ShopSource from ...mods.mod_data import ModNames from ...strings.craftable_names import ModEdible from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit -from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem +from ...strings.fish_names import WaterItem, SVEWaterItem from ...strings.flower_names import Flower from ...strings.food_names import SVEMeal, SVEBeverage from ...strings.forageable_names import Mushroom, Forageable, SVEForage from ...strings.gift_names import SVEGift -from ...strings.metal_names import Ore -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import ModLoot from ...strings.performance_names import Performance -from ...strings.region_names import Region, SVERegion, LogicRegion +from ...strings.region_names import Region, SVERegion from ...strings.season_names import Season from ...strings.seed_names import SVESeed from ...strings.skill_names import Skill @@ -81,7 +80,8 @@ register_mod_content_pack(SVEContentPack( ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), - SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + SVEMeal.grampleton_orange_chicken: ( + ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), @@ -92,7 +92,8 @@ register_mod_content_pack(SVEContentPack( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) ), Mushroom.purple: ( - ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), + ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) ), Mushroom.morel: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) @@ -117,7 +118,8 @@ register_mod_content_pack(SVEContentPack( ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), - other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + other_requirements=( + CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), @@ -137,7 +139,8 @@ register_mod_content_pack(SVEContentPack( SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), - other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + other_requirements=( + CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), # Fable Reef diff --git a/worlds/stardew_valley/content/vanilla/qi_board.py b/worlds/stardew_valley/content/vanilla/qi_board.py index d859d3b16f..e5f67c4319 100644 --- a/worlds/stardew_valley/content/vanilla/qi_board.py +++ b/worlds/stardew_valley/content/vanilla/qi_board.py @@ -6,7 +6,6 @@ from ...data.game_item import GenericSource, ItemTag from ...data.harvest import HarvestCropSource from ...strings.crop_names import Fruit from ...strings.region_names import Region -from ...strings.season_names import Season from ...strings.seed_names import Seed diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 8b2e189c79..75f0f75a23 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -10,7 +10,7 @@ from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Ligh from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem, Trash, all_fish +from ..strings.fish_names import Fish, WaterItem, Trash from ..strings.flower_names import Flower from ..strings.food_names import Beverage, Meal from ..strings.forageable_names import Forageable, Mushroom @@ -832,7 +832,7 @@ calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50), strawberry_seeds.as_amount(20), spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5), - pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),] + pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5)] calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2) raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index 1bb4b2bea7..de371b7c3a 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -14,7 +14,7 @@ from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro from ..strings.fish_names import Fish, WaterItem, ModTrash, Trash from ..strings.flower_names import Flower from ..strings.food_names import Meal -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom +from ..strings.forageable_names import Forageable, DistantLandsForageable, Mushroom from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine @@ -318,7 +318,8 @@ travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 25 preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1, {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology) +restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, + ModNames.archaeology) preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, @@ -330,12 +331,14 @@ glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifac glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology) bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology) rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology) -rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology) +rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, + ModNames.archaeology) bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology) water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology) wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology) hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology) -lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology) +lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, + ModNames.archaeology) volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1}, ModNames.archaeology) haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1, diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index 3123bb9243..667227cb9e 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -1,15 +1,16 @@ from typing import Dict, List, Optional -from ..mods.mod_data import ModNames + from .recipe_source import RecipeSource, FriendshipSource, SkillSource, QueenOfSauceSource, ShopSource, StarterSource, ShopTradeSource, ShopFriendshipSource +from ..mods.mod_data import ModNames from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom -from ..strings.ingredient_names import Ingredient from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal +from ..strings.forageable_names import Forageable, SVEForage, Mushroom +from ..strings.ingredient_names import Ingredient from ..strings.material_names import Material from ..strings.metal_names import Fossil, Artifact from ..strings.monster_drop_names import Loot @@ -45,7 +46,8 @@ def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str return create_recipe(name, ingredients, source, mod_name) -def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: +def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int], + mod_name: Optional[str] = None) -> CookingRecipe: source = ShopFriendshipSource(friend, hearts, region, price) return create_recipe(name, ingredients, source, mod_name) @@ -85,7 +87,8 @@ algae_soup = friendship_recipe(Meal.algae_soup, NPC.clint, 3, {WaterItem.green_a artichoke_dip = queen_of_sauce_recipe(Meal.artichoke_dip, 1, Season.fall, 28, {Vegetable.artichoke: 1, AnimalProduct.cow_milk: 1}) autumn_bounty = friendship_recipe(Meal.autumn_bounty, NPC.demetrius, 7, {Vegetable.yam: 1, Vegetable.pumpkin: 1}) baked_fish = queen_of_sauce_recipe(Meal.baked_fish, 1, Season.summer, 7, {Fish.sunfish: 1, Fish.bream: 1, Ingredient.wheat_flour: 1}) -banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30, {Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1}) +banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30, + {Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1}) bean_hotpot = friendship_recipe(Meal.bean_hotpot, NPC.clint, 7, {Vegetable.green_bean: 2}) blackberry_cobbler_ingredients = {Forageable.blackberry: 2, Ingredient.sugar: 1, Ingredient.wheat_flour: 1} blackberry_cobbler_qos = queen_of_sauce_recipe(Meal.blackberry_cobbler, 2, Season.fall, 14, blackberry_cobbler_ingredients) @@ -181,21 +184,23 @@ vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Ve magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic) baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15, - Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve) + Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve) big_bark_burger = friendship_and_shop_recipe(SVEMeal.big_bark_burger, NPC.gus, 5, Region.saloon, 5500, {SVEFish.puppyfish: 1, Meal.bread: 1, Ingredient.oil: 1}, ModNames.sve) flower_cookie = shop_recipe(SVEMeal.flower_cookie, SVERegion.bear_shop, 0, {SVEForage.ferngill_primrose: 1, SVEForage.goldenrod: 1, - SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1, - AnimalProduct.large_egg: 1}, ModNames.sve) + SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1, + AnimalProduct.large_egg: 1}, ModNames.sve) frog_legs = shop_recipe(SVEMeal.frog_legs, Region.adventurer_guild, 2000, {SVEFish.frog: 1, Ingredient.oil: 1, Ingredient.wheat_flour: 1}, ModNames.sve) glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000, {SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve) mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6, SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, ModNames.sve) -mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, - Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, + {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, + ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, + ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, @@ -205,17 +210,22 @@ mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.gobli Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands) void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands) crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1, - DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands) + DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, + ModNames.distant_lands) pemmican = friendship_recipe(DistantLandsMeal.pemmican, ModNPC.goblin, 8, {Loot.bug_meat: 1, Fish.any: 1, Forageable.salmonberry: 3, Material.stone: 2}, ModNames.distant_lands) special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1, Vegetable.garlic: 1}, ModNames.boarding_house) -diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology) -rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology) -ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology) +diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, + {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology) +rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, + ModNames.archaeology) +ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, + {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, + ModNames.archaeology) grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill) fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill) -all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes} \ No newline at end of file +all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes} diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index ead4d62f16..bc8c09ee92 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -106,7 +106,7 @@ class MasterySource(RecipeSource): self.skill = skill def __repr__(self): - return f"MasterySource at level {self.level} {self.skill}" + return f"MasterySource {self.skill}" class ShopSource(RecipeSource): diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index add99a2c2e..2038d995a7 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -1,7 +1,7 @@ +import typing from typing import Union from .base_logic import BaseLogicMixin, BaseLogic -from .cooking_logic import CookingLogicMixin from .mine_logic import MineLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin @@ -13,6 +13,11 @@ from ..strings.region_names import Region from ..strings.skill_names import Skill, ModSkill from ..strings.tool_names import ToolMaterial, Tool +if typing.TYPE_CHECKING: + from ..mods.logic.mod_logic import ModLogicMixin +else: + ModLogicMixin = object + class AbilityLogicMixin(BaseLogicMixin): def __init__(self, *args, **kwargs): @@ -20,7 +25,8 @@ class AbilityLogicMixin(BaseLogicMixin): self.ability = AbilityLogic(*args, **kwargs) -class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin]]): +class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin, +ModLogicMixin]]): def can_mine_perfectly(self) -> StardewRule: return self.logic.mine.can_progress_in_the_mines_from_floor(160) diff --git a/worlds/stardew_valley/logic/action_logic.py b/worlds/stardew_valley/logic/action_logic.py index dc5deda427..5b117de68c 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -6,7 +6,6 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .tool_logic import ToolLogicMixin -from ..options import ToolProgression from ..stardew_rule import StardewRule, True_ from ..strings.generic_names import Generic from ..strings.geode_names import Geode diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index bc2f6cb126..6d0cd11baf 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -1,3 +1,4 @@ +import typing from functools import cached_property from typing import Union, Tuple @@ -24,6 +25,11 @@ from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills from ..strings.tool_names import ToolMaterial, Tool from ..strings.wallet_item_names import Wallet +if typing.TYPE_CHECKING: + from ..mods.logic.mod_logic import ModLogicMixin +else: + ModLogicMixin = object + fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west) vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") @@ -35,7 +41,7 @@ class SkillLogicMixin(BaseLogicMixin): class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin, -CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): +CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin, ModLogicMixin]]): # Should be cached def can_earn_level(self, skill: str, level: int) -> StardewRule: diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index ef5eab0134..12e824d212 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -2,7 +2,6 @@ from typing import Dict, Union from ..mod_data import ModNames from ... import options -from ...data.craftable_data import all_crafting_recipes_by_name from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin @@ -20,11 +19,9 @@ from ...logic.season_logic import SeasonLogicMixin from ...logic.skill_logic import SkillLogicMixin from ...logic.time_logic import TimeLogicMixin from ...logic.tool_logic import ToolLogicMixin -from ...options import Cropsanity -from ...stardew_rule import StardewRule, True_ +from ...stardew_rule import StardewRule from ...strings.artisan_good_names import ModArtisanGood -from ...strings.craftable_names import ModCraftable, ModMachine -from ...strings.fish_names import ModTrash +from ...strings.craftable_names import ModCraftable from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py index 1aa71404ae..2ff7452394 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -3,8 +3,8 @@ from typing import Dict, Union from ..mod_data import ModNames from ...logic.base_logic import BaseLogic, BaseLogicMixin from ...logic.has_logic import HasLogicMixin -from ...logic.quest_logic import QuestLogicMixin from ...logic.monster_logic import MonsterLogicMixin +from ...logic.quest_logic import QuestLogicMixin from ...logic.received_logic import ReceivedLogicMixin from ...logic.region_logic import RegionLogicMixin from ...logic.relationship_logic import RelationshipLogicMixin @@ -16,7 +16,6 @@ from ...strings.artisan_good_names import ArtisanGood from ...strings.crop_names import Fruit, SVEFruit, SVEVegetable, Vegetable from ...strings.fertilizer_names import Fertilizer from ...strings.food_names import Meal, Beverage -from ...strings.forageable_names import SVEForage from ...strings.material_names import Material from ...strings.metal_names import Ore, MetalBar from ...strings.monster_drop_names import Loot, ModLoot @@ -35,7 +34,7 @@ class ModQuestLogicMixin(BaseLogicMixin): class ModQuestLogic(BaseLogic[Union[HasLogicMixin, QuestLogicMixin, ReceivedLogicMixin, RegionLogicMixin, - TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]): +TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]): def get_modded_quest_rules(self) -> Dict[str, StardewRule]: quests = dict() quests.update(self._get_juna_quest_rules()) diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index d59439a487..7a680d5faa 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -7,7 +7,7 @@ from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag from .strings.entrance_names import Entrance, LogicEntrance -from .strings.region_names import Region, LogicRegion +from .strings.region_names import Region as RegionName, LogicRegion class RegionFactory(Protocol): @@ -16,192 +16,192 @@ class RegionFactory(Protocol): vanilla_regions = [ - RegionData(Region.menu, [Entrance.to_stardew_valley]), - RegionData(Region.stardew_valley, [Entrance.to_farmhouse]), - RegionData(Region.farm_house, + RegionData(RegionName.menu, [Entrance.to_stardew_valley]), + RegionData(RegionName.stardew_valley, [Entrance.to_farmhouse]), + RegionData(RegionName.farm_house, [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]), - RegionData(Region.cellar), - RegionData(Region.farm, + RegionData(RegionName.cellar), + RegionData(RegionName.farm, [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse, Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops, LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]), - RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]), - RegionData(Region.bus_stop, + RegionData(RegionName.backwoods, [Entrance.backwoods_to_mountain]), + RegionData(RegionName.bus_stop, [Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]), - RegionData(Region.forest, + RegionData(RegionName.forest, [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch, Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant, LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby, LogicEntrance.attend_festival_of_ice]), RegionData(LogicRegion.forest_waterfall), - RegionData(Region.farm_cave), - RegionData(Region.greenhouse, + RegionData(RegionName.farm_cave), + RegionData(RegionName.greenhouse, [LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse, LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]), - RegionData(Region.mountain, + RegionData(RegionName.mountain, [Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop, Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_town, Entrance.mountain_to_maru_room, Entrance.mountain_to_leo_treehouse]), - RegionData(Region.leo_treehouse, is_ginger_island=True), - RegionData(Region.maru_room), - RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]), - RegionData(Region.bus_tunnel), - RegionData(Region.town, + RegionData(RegionName.leo_treehouse, is_ginger_island=True), + RegionData(RegionName.maru_room), + RegionData(RegionName.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]), + RegionData(RegionName.bus_tunnel), + RegionData(RegionName.town, [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store, Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house, Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart, Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair, LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]), - RegionData(Region.beach, + RegionData(RegionName.beach, [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau, LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]), - RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]), - RegionData(Region.ranch), - RegionData(Region.leah_house), - RegionData(Region.mastery_cave), - RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]), - RegionData(Region.mutant_bug_lair), - RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), - RegionData(Region.wizard_basement), - RegionData(Region.tent), - RegionData(Region.carpenter, [Entrance.enter_sebastian_room]), - RegionData(Region.sebastian_room), - RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]), - RegionData(Region.adventurer_guild_bedroom), - RegionData(Region.community_center, + RegionData(RegionName.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]), + RegionData(RegionName.ranch), + RegionData(RegionName.leah_house), + RegionData(RegionName.mastery_cave), + RegionData(RegionName.sewer, [Entrance.enter_mutant_bug_lair]), + RegionData(RegionName.mutant_bug_lair), + RegionData(RegionName.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), + RegionData(RegionName.wizard_basement), + RegionData(RegionName.tent), + RegionData(RegionName.carpenter, [Entrance.enter_sebastian_room]), + RegionData(RegionName.sebastian_room), + RegionData(RegionName.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]), + RegionData(RegionName.adventurer_guild_bedroom), + RegionData(RegionName.community_center, [Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank, Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]), - RegionData(Region.crafts_room), - RegionData(Region.pantry), - RegionData(Region.fish_tank), - RegionData(Region.boiler_room), - RegionData(Region.bulletin_board), - RegionData(Region.vault), - RegionData(Region.hospital, [Entrance.enter_harvey_room]), - RegionData(Region.harvey_room), - RegionData(Region.pierre_store, [Entrance.enter_sunroom]), - RegionData(Region.sunroom), - RegionData(Region.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]), - RegionData(Region.jotpk_world_1, [Entrance.reach_jotpk_world_2]), - RegionData(Region.jotpk_world_2, [Entrance.reach_jotpk_world_3]), - RegionData(Region.jotpk_world_3), - RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]), - RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]), - RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]), - RegionData(Region.junimo_kart_4), - RegionData(Region.alex_house), - RegionData(Region.trailer), - RegionData(Region.mayor_house), - RegionData(Region.sam_house), - RegionData(Region.haley_house), - RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]), - RegionData(Region.museum), - RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]), - RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]), - RegionData(Region.movie_ticket_stand), - RegionData(Region.movie_theater), - RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]), - RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True), - RegionData(Region.elliott_house), - RegionData(Region.tide_pools), - RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]), - RegionData(Region.locker_room, [Entrance.enter_public_bath]), - RegionData(Region.public_bath), - RegionData(Region.witch_warp_cave, [Entrance.enter_witch_swamp]), - RegionData(Region.witch_swamp, [Entrance.enter_witch_hut]), - RegionData(Region.witch_hut, [Entrance.witch_warp_to_wizard_basement]), - RegionData(Region.quarry, [Entrance.enter_quarry_mine_entrance]), - RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]), - RegionData(Region.quarry_mine), - RegionData(Region.secret_woods), - RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]), - RegionData(Region.oasis, [Entrance.enter_casino]), - RegionData(Region.casino), - RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]), - RegionData(Region.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]), - RegionData(Region.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]), - RegionData(Region.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]), - RegionData(Region.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]), - RegionData(Region.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]), - RegionData(Region.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]), - RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]), - RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]), - RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]), - RegionData(Region.dangerous_skull_cavern, is_ginger_island=True), - RegionData(Region.island_south, + RegionData(RegionName.crafts_room), + RegionData(RegionName.pantry), + RegionData(RegionName.fish_tank), + RegionData(RegionName.boiler_room), + RegionData(RegionName.bulletin_board), + RegionData(RegionName.vault), + RegionData(RegionName.hospital, [Entrance.enter_harvey_room]), + RegionData(RegionName.harvey_room), + RegionData(RegionName.pierre_store, [Entrance.enter_sunroom]), + RegionData(RegionName.sunroom), + RegionData(RegionName.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]), + RegionData(RegionName.jotpk_world_1, [Entrance.reach_jotpk_world_2]), + RegionData(RegionName.jotpk_world_2, [Entrance.reach_jotpk_world_3]), + RegionData(RegionName.jotpk_world_3), + RegionData(RegionName.junimo_kart_1, [Entrance.reach_junimo_kart_2]), + RegionData(RegionName.junimo_kart_2, [Entrance.reach_junimo_kart_3]), + RegionData(RegionName.junimo_kart_3, [Entrance.reach_junimo_kart_4]), + RegionData(RegionName.junimo_kart_4), + RegionData(RegionName.alex_house), + RegionData(RegionName.trailer), + RegionData(RegionName.mayor_house), + RegionData(RegionName.sam_house), + RegionData(RegionName.haley_house), + RegionData(RegionName.blacksmith, [LogicEntrance.blacksmith_copper]), + RegionData(RegionName.museum), + RegionData(RegionName.jojamart, [Entrance.enter_abandoned_jojamart]), + RegionData(RegionName.abandoned_jojamart, [Entrance.enter_movie_theater]), + RegionData(RegionName.movie_ticket_stand), + RegionData(RegionName.movie_theater), + RegionData(RegionName.fish_shop, [Entrance.fish_shop_to_boat_tunnel]), + RegionData(RegionName.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True), + RegionData(RegionName.elliott_house), + RegionData(RegionName.tide_pools), + RegionData(RegionName.bathhouse_entrance, [Entrance.enter_locker_room]), + RegionData(RegionName.locker_room, [Entrance.enter_public_bath]), + RegionData(RegionName.public_bath), + RegionData(RegionName.witch_warp_cave, [Entrance.enter_witch_swamp]), + RegionData(RegionName.witch_swamp, [Entrance.enter_witch_hut]), + RegionData(RegionName.witch_hut, [Entrance.witch_warp_to_wizard_basement]), + RegionData(RegionName.quarry, [Entrance.enter_quarry_mine_entrance]), + RegionData(RegionName.quarry_mine_entrance, [Entrance.enter_quarry_mine]), + RegionData(RegionName.quarry_mine), + RegionData(RegionName.secret_woods), + RegionData(RegionName.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]), + RegionData(RegionName.oasis, [Entrance.enter_casino]), + RegionData(RegionName.casino), + RegionData(RegionName.skull_cavern_entrance, [Entrance.enter_skull_cavern]), + RegionData(RegionName.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]), + RegionData(RegionName.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]), + RegionData(RegionName.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]), + RegionData(RegionName.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]), + RegionData(RegionName.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]), + RegionData(RegionName.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]), + RegionData(RegionName.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]), + RegionData(RegionName.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]), + RegionData(RegionName.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]), + RegionData(RegionName.dangerous_skull_cavern, is_ginger_island=True), + RegionData(RegionName.island_south, [Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast, Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site, Entrance.parrot_express_docks_to_jungle], is_ginger_island=True), - RegionData(Region.island_resort, is_ginger_island=True), - RegionData(Region.island_west, + RegionData(RegionName.island_resort, is_ginger_island=True), + RegionData(RegionName.island_west, [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island, LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, LogicEntrance.grow_indoor_crops_on_island], is_ginger_island=True), - RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), - RegionData(Region.island_shrine, is_ginger_island=True), - RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True), - RegionData(Region.island_north, + RegionData(RegionName.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), + RegionData(RegionName.island_shrine, is_ginger_island=True), + RegionData(RegionName.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True), + RegionData(RegionName.island_north, [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks], is_ginger_island=True), - RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True), - RegionData(Region.volcano_secret_beach, is_ginger_island=True), - RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True), - RegionData(Region.volcano_dwarf_shop, is_ginger_island=True), - RegionData(Region.volcano_floor_10, is_ginger_island=True), - RegionData(Region.island_trader, is_ginger_island=True), - RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True), - RegionData(Region.gourmand_frog_cave, is_ginger_island=True), - RegionData(Region.colored_crystals_cave, is_ginger_island=True), - RegionData(Region.shipwreck, is_ginger_island=True), - RegionData(Region.qi_walnut_room, is_ginger_island=True), - RegionData(Region.leo_hut, is_ginger_island=True), - RegionData(Region.pirate_cove, is_ginger_island=True), - RegionData(Region.field_office, is_ginger_island=True), - RegionData(Region.dig_site, + RegionData(RegionName.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True), + RegionData(RegionName.volcano_secret_beach, is_ginger_island=True), + RegionData(RegionName.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True), + RegionData(RegionName.volcano_dwarf_shop, is_ginger_island=True), + RegionData(RegionName.volcano_floor_10, is_ginger_island=True), + RegionData(RegionName.island_trader, is_ginger_island=True), + RegionData(RegionName.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True), + RegionData(RegionName.gourmand_frog_cave, is_ginger_island=True), + RegionData(RegionName.colored_crystals_cave, is_ginger_island=True), + RegionData(RegionName.shipwreck, is_ginger_island=True), + RegionData(RegionName.qi_walnut_room, is_ginger_island=True), + RegionData(RegionName.leo_hut, is_ginger_island=True), + RegionData(RegionName.pirate_cove, is_ginger_island=True), + RegionData(RegionName.field_office, is_ginger_island=True), + RegionData(RegionName.dig_site, [Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano, Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle], is_ginger_island=True), - RegionData(Region.professor_snail_cave, is_ginger_island=True), - RegionData(Region.coop), - RegionData(Region.barn), - RegionData(Region.shed), - RegionData(Region.slime_hutch), + RegionData(RegionName.professor_snail_cave, is_ginger_island=True), + RegionData(RegionName.coop), + RegionData(RegionName.barn), + RegionData(RegionName.shed), + RegionData(RegionName.slime_hutch), - RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf, - Entrance.dig_to_mines_floor_5]), - RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]), - RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]), - RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]), - RegionData(Region.mines_floor_20, [Entrance.dig_to_mines_floor_25]), - RegionData(Region.mines_floor_25, [Entrance.dig_to_mines_floor_30]), - RegionData(Region.mines_floor_30, [Entrance.dig_to_mines_floor_35]), - RegionData(Region.mines_floor_35, [Entrance.dig_to_mines_floor_40]), - RegionData(Region.mines_floor_40, [Entrance.dig_to_mines_floor_45]), - RegionData(Region.mines_floor_45, [Entrance.dig_to_mines_floor_50]), - RegionData(Region.mines_floor_50, [Entrance.dig_to_mines_floor_55]), - RegionData(Region.mines_floor_55, [Entrance.dig_to_mines_floor_60]), - RegionData(Region.mines_floor_60, [Entrance.dig_to_mines_floor_65]), - RegionData(Region.mines_floor_65, [Entrance.dig_to_mines_floor_70]), - RegionData(Region.mines_floor_70, [Entrance.dig_to_mines_floor_75]), - RegionData(Region.mines_floor_75, [Entrance.dig_to_mines_floor_80]), - RegionData(Region.mines_floor_80, [Entrance.dig_to_mines_floor_85]), - RegionData(Region.mines_floor_85, [Entrance.dig_to_mines_floor_90]), - RegionData(Region.mines_floor_90, [Entrance.dig_to_mines_floor_95]), - RegionData(Region.mines_floor_95, [Entrance.dig_to_mines_floor_100]), - RegionData(Region.mines_floor_100, [Entrance.dig_to_mines_floor_105]), - RegionData(Region.mines_floor_105, [Entrance.dig_to_mines_floor_110]), - RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]), - RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]), - RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]), - RegionData(Region.dangerous_mines_20, is_ginger_island=True), - RegionData(Region.dangerous_mines_60, is_ginger_island=True), - RegionData(Region.dangerous_mines_100, is_ginger_island=True), + RegionData(RegionName.mines, [LogicEntrance.talk_to_mines_dwarf, + Entrance.dig_to_mines_floor_5]), + RegionData(RegionName.mines_floor_5, [Entrance.dig_to_mines_floor_10]), + RegionData(RegionName.mines_floor_10, [Entrance.dig_to_mines_floor_15]), + RegionData(RegionName.mines_floor_15, [Entrance.dig_to_mines_floor_20]), + RegionData(RegionName.mines_floor_20, [Entrance.dig_to_mines_floor_25]), + RegionData(RegionName.mines_floor_25, [Entrance.dig_to_mines_floor_30]), + RegionData(RegionName.mines_floor_30, [Entrance.dig_to_mines_floor_35]), + RegionData(RegionName.mines_floor_35, [Entrance.dig_to_mines_floor_40]), + RegionData(RegionName.mines_floor_40, [Entrance.dig_to_mines_floor_45]), + RegionData(RegionName.mines_floor_45, [Entrance.dig_to_mines_floor_50]), + RegionData(RegionName.mines_floor_50, [Entrance.dig_to_mines_floor_55]), + RegionData(RegionName.mines_floor_55, [Entrance.dig_to_mines_floor_60]), + RegionData(RegionName.mines_floor_60, [Entrance.dig_to_mines_floor_65]), + RegionData(RegionName.mines_floor_65, [Entrance.dig_to_mines_floor_70]), + RegionData(RegionName.mines_floor_70, [Entrance.dig_to_mines_floor_75]), + RegionData(RegionName.mines_floor_75, [Entrance.dig_to_mines_floor_80]), + RegionData(RegionName.mines_floor_80, [Entrance.dig_to_mines_floor_85]), + RegionData(RegionName.mines_floor_85, [Entrance.dig_to_mines_floor_90]), + RegionData(RegionName.mines_floor_90, [Entrance.dig_to_mines_floor_95]), + RegionData(RegionName.mines_floor_95, [Entrance.dig_to_mines_floor_100]), + RegionData(RegionName.mines_floor_100, [Entrance.dig_to_mines_floor_105]), + RegionData(RegionName.mines_floor_105, [Entrance.dig_to_mines_floor_110]), + RegionData(RegionName.mines_floor_110, [Entrance.dig_to_mines_floor_115]), + RegionData(RegionName.mines_floor_115, [Entrance.dig_to_mines_floor_120]), + RegionData(RegionName.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]), + RegionData(RegionName.dangerous_mines_20, is_ginger_island=True), + RegionData(RegionName.dangerous_mines_60, is_ginger_island=True), + RegionData(RegionName.dangerous_mines_100, is_ginger_island=True), RegionData(LogicRegion.mines_dwarf_shop), RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]), @@ -256,206 +256,207 @@ vanilla_regions = [ # Exists and where they lead vanilla_connections = [ - ConnectionData(Entrance.to_stardew_valley, Region.stardew_valley), - ConnectionData(Entrance.to_farmhouse, Region.farm_house), - ConnectionData(Entrance.farmhouse_to_farm, Region.farm), - ConnectionData(Entrance.downstairs_to_cellar, Region.cellar), - ConnectionData(Entrance.farm_to_backwoods, Region.backwoods), - ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop), - ConnectionData(Entrance.farm_to_forest, Region.forest), - ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData(Entrance.enter_greenhouse, Region.greenhouse), - ConnectionData(Entrance.enter_coop, Region.coop), - ConnectionData(Entrance.enter_barn, Region.barn), - ConnectionData(Entrance.enter_shed, Region.shed), - ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch), - ConnectionData(Entrance.use_desert_obelisk, Region.desert), - ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.use_farm_obelisk, Region.farm), - ConnectionData(Entrance.backwoods_to_mountain, Region.mountain), - ConnectionData(Entrance.bus_stop_to_town, Region.town), - ConnectionData(Entrance.bus_stop_to_tunnel_entrance, Region.tunnel_entrance), - ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, Region.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData(Entrance.take_bus_to_desert, Region.desert), - ConnectionData(Entrance.forest_to_town, Region.town), - ConnectionData(Entrance.forest_to_wizard_tower, Region.wizard_tower, + ConnectionData(Entrance.to_stardew_valley, RegionName.stardew_valley), + ConnectionData(Entrance.to_farmhouse, RegionName.farm_house), + ConnectionData(Entrance.farmhouse_to_farm, RegionName.farm), + ConnectionData(Entrance.downstairs_to_cellar, RegionName.cellar), + ConnectionData(Entrance.farm_to_backwoods, RegionName.backwoods), + ConnectionData(Entrance.farm_to_bus_stop, RegionName.bus_stop), + ConnectionData(Entrance.farm_to_forest, RegionName.forest), + ConnectionData(Entrance.farm_to_farmcave, RegionName.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData(Entrance.enter_greenhouse, RegionName.greenhouse), + ConnectionData(Entrance.enter_coop, RegionName.coop), + ConnectionData(Entrance.enter_barn, RegionName.barn), + ConnectionData(Entrance.enter_shed, RegionName.shed), + ConnectionData(Entrance.enter_slime_hutch, RegionName.slime_hutch), + ConnectionData(Entrance.use_desert_obelisk, RegionName.desert), + ConnectionData(Entrance.use_island_obelisk, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.use_farm_obelisk, RegionName.farm), + ConnectionData(Entrance.backwoods_to_mountain, RegionName.mountain), + ConnectionData(Entrance.bus_stop_to_town, RegionName.town), + ConnectionData(Entrance.bus_stop_to_tunnel_entrance, RegionName.tunnel_entrance), + ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, RegionName.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData(Entrance.take_bus_to_desert, RegionName.desert), + ConnectionData(Entrance.forest_to_town, RegionName.town), + ConnectionData(Entrance.forest_to_wizard_tower, RegionName.wizard_tower, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.forest_to_marnie_ranch, Region.ranch, + ConnectionData(Entrance.enter_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.forest_to_marnie_ranch, RegionName.ranch, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.forest_to_leah_cottage, Region.leah_house, + ConnectionData(Entrance.forest_to_leah_cottage, RegionName.leah_house, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_secret_woods, Region.secret_woods), - ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES), - ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.mountain_to_railroad, Region.railroad), - ConnectionData(Entrance.mountain_to_tent, Region.tent, + ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods), + ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES), + ConnectionData(Entrance.town_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_mutant_bug_lair, RegionName.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.mountain_to_railroad, RegionName.railroad), + ConnectionData(Entrance.mountain_to_tent, RegionName.tent, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.mountain_to_leo_treehouse, Region.leo_treehouse, + ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.mountain_to_carpenter_shop, Region.carpenter, + ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.mountain_to_maru_room, Region.maru_room, + ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, + ConnectionData(Entrance.enter_sebastian_room, RegionName.sebastian_room, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.mountain_to_adventurer_guild, RegionName.adventurer_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom), - ConnectionData(Entrance.enter_quarry, Region.quarry), - ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance, + ConnectionData(Entrance.adventurer_guild_to_bedroom, RegionName.adventurer_guild_bedroom), + ConnectionData(Entrance.enter_quarry, RegionName.quarry), + ConnectionData(Entrance.enter_quarry_mine_entrance, RegionName.quarry_mine_entrance, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_quarry_mine, Region.quarry_mine), - ConnectionData(Entrance.mountain_to_town, Region.town), - ConnectionData(Entrance.town_to_community_center, Region.community_center, + ConnectionData(Entrance.enter_quarry_mine, RegionName.quarry_mine), + ConnectionData(Entrance.mountain_to_town, RegionName.town), + ConnectionData(Entrance.town_to_community_center, RegionName.community_center, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.access_crafts_room, Region.crafts_room), - ConnectionData(Entrance.access_pantry, Region.pantry), - ConnectionData(Entrance.access_fish_tank, Region.fish_tank), - ConnectionData(Entrance.access_boiler_room, Region.boiler_room), - ConnectionData(Entrance.access_bulletin_board, Region.bulletin_board), - ConnectionData(Entrance.access_vault, Region.vault), - ConnectionData(Entrance.town_to_hospital, Region.hospital, + ConnectionData(Entrance.access_crafts_room, RegionName.crafts_room), + ConnectionData(Entrance.access_pantry, RegionName.pantry), + ConnectionData(Entrance.access_fish_tank, RegionName.fish_tank), + ConnectionData(Entrance.access_boiler_room, RegionName.boiler_room), + ConnectionData(Entrance.access_bulletin_board, RegionName.bulletin_board), + ConnectionData(Entrance.access_vault, RegionName.vault), + ConnectionData(Entrance.town_to_hospital, RegionName.hospital, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_harvey_room, Region.harvey_room, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.town_to_pierre_general_store, Region.pierre_store, + ConnectionData(Entrance.enter_harvey_room, RegionName.harvey_room, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.town_to_pierre_general_store, RegionName.pierre_store, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith, + ConnectionData(Entrance.enter_sunroom, RegionName.sunroom, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.town_to_clint_blacksmith, RegionName.blacksmith, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_saloon, Region.saloon, + ConnectionData(Entrance.town_to_saloon, RegionName.saloon, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1), - ConnectionData(Entrance.reach_jotpk_world_2, Region.jotpk_world_2), - ConnectionData(Entrance.reach_jotpk_world_3, Region.jotpk_world_3), - ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1), - ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2), - ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3), - ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4), - ConnectionData(Entrance.town_to_sam_house, Region.sam_house, + ConnectionData(Entrance.play_journey_of_the_prairie_king, RegionName.jotpk_world_1), + ConnectionData(Entrance.reach_jotpk_world_2, RegionName.jotpk_world_2), + ConnectionData(Entrance.reach_jotpk_world_3, RegionName.jotpk_world_3), + ConnectionData(Entrance.play_junimo_kart, RegionName.junimo_kart_1), + ConnectionData(Entrance.reach_junimo_kart_2, RegionName.junimo_kart_2), + ConnectionData(Entrance.reach_junimo_kart_3, RegionName.junimo_kart_3), + ConnectionData(Entrance.reach_junimo_kart_4, RegionName.junimo_kart_4), + ConnectionData(Entrance.town_to_sam_house, RegionName.sam_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_haley_house, Region.haley_house, + ConnectionData(Entrance.town_to_haley_house, RegionName.haley_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_mayor_manor, Region.mayor_house, + ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_alex_house, Region.alex_house, + ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_trailer, Region.trailer, + ConnectionData(Entrance.town_to_trailer, RegionName.trailer, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_museum, Region.museum, + ConnectionData(Entrance.town_to_museum, RegionName.museum, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.town_to_jojamart, Region.jojamart, + ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.purchase_movie_ticket, Region.movie_ticket_stand), - ConnectionData(Entrance.enter_abandoned_jojamart, Region.abandoned_jojamart), - ConnectionData(Entrance.enter_movie_theater, Region.movie_theater), - ConnectionData(Entrance.town_to_beach, Region.beach), - ConnectionData(Entrance.enter_elliott_house, Region.elliott_house, + ConnectionData(Entrance.purchase_movie_ticket, RegionName.movie_ticket_stand), + ConnectionData(Entrance.enter_abandoned_jojamart, RegionName.abandoned_jojamart), + ConnectionData(Entrance.enter_movie_theater, RegionName.movie_theater), + ConnectionData(Entrance.town_to_beach, RegionName.beach), + ConnectionData(Entrance.enter_elliott_house, RegionName.elliott_house, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.beach_to_willy_fish_shop, Region.fish_shop, + ConnectionData(Entrance.beach_to_willy_fish_shop, RegionName.fish_shop, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.fish_shop_to_boat_tunnel, Region.boat_tunnel, + ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.enter_tide_pools, Region.tide_pools), - ConnectionData(Entrance.mountain_to_the_mines, Region.mines, + ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.enter_tide_pools, RegionName.tide_pools), + ConnectionData(Entrance.mountain_to_the_mines, RegionName.mines, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5), - ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10), - ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15), - ConnectionData(Entrance.dig_to_mines_floor_20, Region.mines_floor_20), - ConnectionData(Entrance.dig_to_mines_floor_25, Region.mines_floor_25), - ConnectionData(Entrance.dig_to_mines_floor_30, Region.mines_floor_30), - ConnectionData(Entrance.dig_to_mines_floor_35, Region.mines_floor_35), - ConnectionData(Entrance.dig_to_mines_floor_40, Region.mines_floor_40), - ConnectionData(Entrance.dig_to_mines_floor_45, Region.mines_floor_45), - ConnectionData(Entrance.dig_to_mines_floor_50, Region.mines_floor_50), - ConnectionData(Entrance.dig_to_mines_floor_55, Region.mines_floor_55), - ConnectionData(Entrance.dig_to_mines_floor_60, Region.mines_floor_60), - ConnectionData(Entrance.dig_to_mines_floor_65, Region.mines_floor_65), - ConnectionData(Entrance.dig_to_mines_floor_70, Region.mines_floor_70), - ConnectionData(Entrance.dig_to_mines_floor_75, Region.mines_floor_75), - ConnectionData(Entrance.dig_to_mines_floor_80, Region.mines_floor_80), - ConnectionData(Entrance.dig_to_mines_floor_85, Region.mines_floor_85), - ConnectionData(Entrance.dig_to_mines_floor_90, Region.mines_floor_90), - ConnectionData(Entrance.dig_to_mines_floor_95, Region.mines_floor_95), - ConnectionData(Entrance.dig_to_mines_floor_100, Region.mines_floor_100), - ConnectionData(Entrance.dig_to_mines_floor_105, Region.mines_floor_105), - ConnectionData(Entrance.dig_to_mines_floor_110, Region.mines_floor_110), - ConnectionData(Entrance.dig_to_mines_floor_115, Region.mines_floor_115), - ConnectionData(Entrance.dig_to_mines_floor_120, Region.mines_floor_120), - ConnectionData(Entrance.dig_to_dangerous_mines_20, Region.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.dig_to_dangerous_mines_60, Region.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.dig_to_dangerous_mines_100, Region.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.enter_skull_cavern_entrance, Region.skull_cavern_entrance, + ConnectionData(Entrance.dig_to_mines_floor_5, RegionName.mines_floor_5), + ConnectionData(Entrance.dig_to_mines_floor_10, RegionName.mines_floor_10), + ConnectionData(Entrance.dig_to_mines_floor_15, RegionName.mines_floor_15), + ConnectionData(Entrance.dig_to_mines_floor_20, RegionName.mines_floor_20), + ConnectionData(Entrance.dig_to_mines_floor_25, RegionName.mines_floor_25), + ConnectionData(Entrance.dig_to_mines_floor_30, RegionName.mines_floor_30), + ConnectionData(Entrance.dig_to_mines_floor_35, RegionName.mines_floor_35), + ConnectionData(Entrance.dig_to_mines_floor_40, RegionName.mines_floor_40), + ConnectionData(Entrance.dig_to_mines_floor_45, RegionName.mines_floor_45), + ConnectionData(Entrance.dig_to_mines_floor_50, RegionName.mines_floor_50), + ConnectionData(Entrance.dig_to_mines_floor_55, RegionName.mines_floor_55), + ConnectionData(Entrance.dig_to_mines_floor_60, RegionName.mines_floor_60), + ConnectionData(Entrance.dig_to_mines_floor_65, RegionName.mines_floor_65), + ConnectionData(Entrance.dig_to_mines_floor_70, RegionName.mines_floor_70), + ConnectionData(Entrance.dig_to_mines_floor_75, RegionName.mines_floor_75), + ConnectionData(Entrance.dig_to_mines_floor_80, RegionName.mines_floor_80), + ConnectionData(Entrance.dig_to_mines_floor_85, RegionName.mines_floor_85), + ConnectionData(Entrance.dig_to_mines_floor_90, RegionName.mines_floor_90), + ConnectionData(Entrance.dig_to_mines_floor_95, RegionName.mines_floor_95), + ConnectionData(Entrance.dig_to_mines_floor_100, RegionName.mines_floor_100), + ConnectionData(Entrance.dig_to_mines_floor_105, RegionName.mines_floor_105), + ConnectionData(Entrance.dig_to_mines_floor_110, RegionName.mines_floor_110), + ConnectionData(Entrance.dig_to_mines_floor_115, RegionName.mines_floor_115), + ConnectionData(Entrance.dig_to_mines_floor_120, RegionName.mines_floor_120), + ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_oasis, Region.oasis, + ConnectionData(Entrance.enter_oasis, RegionName.oasis, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_casino, Region.casino, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_skull_cavern, Region.skull_cavern), - ConnectionData(Entrance.mine_to_skull_cavern_floor_25, Region.skull_cavern_25), - ConnectionData(Entrance.mine_to_skull_cavern_floor_50, Region.skull_cavern_50), - ConnectionData(Entrance.mine_to_skull_cavern_floor_75, Region.skull_cavern_75), - ConnectionData(Entrance.mine_to_skull_cavern_floor_100, Region.skull_cavern_100), - ConnectionData(Entrance.mine_to_skull_cavern_floor_125, Region.skull_cavern_125), - ConnectionData(Entrance.mine_to_skull_cavern_floor_150, Region.skull_cavern_150), - ConnectionData(Entrance.mine_to_skull_cavern_floor_175, Region.skull_cavern_175), - ConnectionData(Entrance.mine_to_skull_cavern_floor_200, Region.skull_cavern_200), - ConnectionData(Entrance.enter_dangerous_skull_cavern, Region.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.enter_witch_warp_cave, Region.witch_warp_cave, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_witch_swamp, Region.witch_swamp, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_witch_hut, Region.witch_hut, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.witch_warp_to_wizard_basement, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_bathhouse_entrance, Region.bathhouse_entrance, + ConnectionData(Entrance.enter_casino, RegionName.casino, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_skull_cavern, RegionName.skull_cavern), + ConnectionData(Entrance.mine_to_skull_cavern_floor_25, RegionName.skull_cavern_25), + ConnectionData(Entrance.mine_to_skull_cavern_floor_50, RegionName.skull_cavern_50), + ConnectionData(Entrance.mine_to_skull_cavern_floor_75, RegionName.skull_cavern_75), + ConnectionData(Entrance.mine_to_skull_cavern_floor_100, RegionName.skull_cavern_100), + ConnectionData(Entrance.mine_to_skull_cavern_floor_125, RegionName.skull_cavern_125), + ConnectionData(Entrance.mine_to_skull_cavern_floor_150, RegionName.skull_cavern_150), + ConnectionData(Entrance.mine_to_skull_cavern_floor_175, RegionName.skull_cavern_175), + ConnectionData(Entrance.mine_to_skull_cavern_floor_200, RegionName.skull_cavern_200), + ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.enter_witch_warp_cave, RegionName.witch_warp_cave, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_witch_swamp, RegionName.witch_swamp, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_witch_hut, RegionName.witch_hut, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.witch_warp_to_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_bathhouse_entrance, RegionName.bathhouse_entrance, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.enter_locker_room, Region.locker_room, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.enter_public_bath, Region.public_bath, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.island_south_to_west, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_south_to_north, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_south_to_east, Region.island_east, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_south_to_southeast, Region.island_south_east, + ConnectionData(Entrance.enter_locker_room, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS), + ConnectionData(Entrance.island_south_to_west, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.island_south_to_north, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.island_south_to_east, RegionName.island_east, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse, + ConnectionData(Entrance.use_island_resort, RegionName.island_resort, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave, + ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave, + ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_west_to_shipwreck, Region.shipwreck, + ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_west_to_qi_walnut_room, Region.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_east_to_leo_hut, Region.leo_hut, + ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_east_to_island_shrine, Region.island_shrine, + ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_southeast_to_pirate_cove, Region.pirate_cove, + ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_north_to_field_office, Region.field_office, + ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_north_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.dig_site_to_professor_snail_cave, Region.professor_snail_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_north_to_volcano, Region.volcano, + ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.volcano_to_secret_beach, Region.volcano_secret_beach, + ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.talk_to_island_trader, Region.island_trader, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.climb_to_volcano_5, Region.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.talk_to_volcano_dwarf, Region.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.climb_to_volcano_10, Region.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_jungle_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_dig_site_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_volcano_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_volcano_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_docks_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_dig_site_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_docks_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_volcano_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_jungle_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop), @@ -708,7 +709,7 @@ def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str, def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool: - if region_name == Region.menu: + if region_name == RegionName.menu: return True for connection in connections_in_slot: if region_name == connection.destination: @@ -718,11 +719,11 @@ def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[C def find_reachable_regions(regions_by_name, connections_by_name, randomized_connections: Dict[ConnectionData, ConnectionData]): - reachable_regions = {Region.menu} + reachable_regions = {RegionName.menu} unreachable_regions = {region for region in regions_by_name.keys()} # unreachable_regions = {region for region in regions_by_name.keys() if region_should_be_reachable(region, connections_by_name.values())} - unreachable_regions.remove(Region.menu) - exits_to_explore = list(regions_by_name[Region.menu].exits) + unreachable_regions.remove(RegionName.menu) + exits_to_explore = list(regions_by_name[RegionName.menu].exits) while exits_to_explore: exit_name = exits_to_explore.pop() # if exit_name not in connections_by_name: diff --git a/worlds/stardew_valley/scripts/update_data.py b/worlds/stardew_valley/scripts/update_data.py index ae8f7f8d55..5c2e6a57a4 100644 --- a/worlds/stardew_valley/scripts/update_data.py +++ b/worlds/stardew_valley/scripts/update_data.py @@ -12,7 +12,7 @@ from typing import List from worlds.stardew_valley import LocationData from worlds.stardew_valley.items import load_item_csv, Group, ItemData -from worlds.stardew_valley.locations import load_location_csv, LocationTags +from worlds.stardew_valley.locations import load_location_csv RESOURCE_PACK_CODE_OFFSET = 5000 script_folder = Path(__file__) @@ -56,9 +56,9 @@ if __name__ == "__main__": and item.code_without_offset is not None) + 1) resource_pack_counter = itertools.count(max(item.code_without_offset - for item in loaded_items - if Group.RESOURCE_PACK in item.groups - and item.code_without_offset is not None) + 1) + for item in loaded_items + if Group.RESOURCE_PACK in item.groups + and item.code_without_offset is not None) + 1) items_to_write = [] for item in loaded_items: if item.code_without_offset is None: diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index af4c3c3533..ff1fbba376 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from functools import cached_property from itertools import chain from threading import Lock -from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional +from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional, cast from BaseClasses import CollectionState from .literal import true_, false_, LiteralStardewRule @@ -318,6 +318,7 @@ class Or(AggregatingStardewRule): return Or(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state) if type(other) is Or: + other = cast(Or, other) return Or(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules), _simplification_state=self.simplification_state.merge(other.simplification_state)) @@ -344,6 +345,7 @@ class And(AggregatingStardewRule): return And(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state) if type(other) is And: + other = cast(And, other) return And(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules), _simplification_state=self.simplification_state.merge(other.simplification_state)) diff --git a/worlds/stardew_valley/test/TestMultiplePlayers.py b/worlds/stardew_valley/test/TestMultiplePlayers.py index 2f2092fdf7..d8db616f66 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -53,8 +53,6 @@ class TestDifferentSettings(SVTestCase): def test_money_rule_caching(self): options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy, StartingMoney.internal_name: 5000} - options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy, - StartingMoney.internal_name: 5000} multiplayer_options = [options_festivals_limited_money, options_festivals_limited_money] multiworld = setup_multiworld(multiplayer_options) diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py index c1e8c2c8f0..da17d749ea 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -25,7 +25,7 @@ class TestWalnutsanityNone(SVTestBase): self.collect("Island Obelisk") self.collect("Island West Turtle") self.collect("Progressive House") - items = self.collect("5 Golden Walnuts", 10) + self.collect("5 Golden Walnuts", 10) self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) self.collect("Island North Turtle") @@ -126,10 +126,10 @@ class TestWalnutsanityPuzzlesAndBushes(SVTestBase): # You need to receive 25, and collect 15 self.collect("Island Obelisk") self.collect("Island West Turtle") - items = self.collect("5 Golden Walnuts", 5) + self.collect("5 Golden Walnuts", 5) self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) - items = self.collect("Island North Turtle") + self.collect("Island North Turtle") self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) @@ -203,7 +203,7 @@ class TestWalnutsanityAll(SVTestBase): self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) self.remove(items) self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) - items = self.collect("5 Golden Walnuts", 4) - items = self.collect("3 Golden Walnuts", 6) - items = self.collect("Golden Walnut", 2) + self.collect("5 Golden Walnuts", 4) + self.collect("3 Golden Walnuts", 6) + self.collect("Golden Walnut", 2) self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) diff --git a/worlds/stardew_valley/test/rules/TestFishing.py b/worlds/stardew_valley/test/rules/TestFishing.py index 04a1528dd8..513bb951e9 100644 --- a/worlds/stardew_valley/test/rules/TestFishing.py +++ b/worlds/stardew_valley/test/rules/TestFishing.py @@ -1,5 +1,4 @@ -from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \ - ElevatorProgression, SpecialOrderLocations +from ...options import SeasonRandomization, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, ElevatorProgression, SpecialOrderLocations from ...strings.fish_names import Fish from ...test import SVTestBase From 894732be474a63f84783de6cfad2260a047e8ad8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 2 Feb 2025 02:53:16 +0100 Subject: [PATCH 57/57] kvui: set home folder to non-default (#4590) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- kvui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvui.py b/kvui.py index f47e45b93c..6718e48bee 100644 --- a/kvui.py +++ b/kvui.py @@ -26,6 +26,10 @@ import Utils if Utils.is_frozen(): os.environ["KIVY_DATA_DIR"] = Utils.local_path("data") +import platformdirs +os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy") +os.makedirs(os.environ["KIVY_HOME"], exist_ok=True) + from kivy.config import Config Config.set("input", "mouse", "mouse,disable_multitouch")